From 10418a079f59a8e7c305cb11b762875da6c9bbf9 Mon Sep 17 00:00:00 2001 From: Paul Hammant Date: Sat, 7 Sep 2013 08:59:02 -0500 Subject: [PATCH] port of protractor locators to new WebDriver JS locator style --- .gitignore | 5 + LICENSE | 23 +++ pom.xml | 49 +++++ .../ngwebdriver/AngularModelAccessor.java | 42 +++++ .../ngwebdriver/ByAngularBinding.java | 49 +++++ .../ngwebdriver/ByAngularRepeater.java | 59 ++++++ .../ngwebdriver/ByAngularRepeaterCell.java | 72 ++++++++ .../ngwebdriver/ByAngularRepeaterColumn.java | 71 ++++++++ .../ngwebdriver/ByAngularRepeaterRow.java | 56 ++++++ .../WaitForAngularRequestsToFinish.java | 11 ++ .../ngwebdriver/AngularAndWebDriverTest.java | 169 ++++++++++++++++++ 11 files changed, 606 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 pom.xml create mode 100644 src/main/java/com/paulhammant/ngwebdriver/AngularModelAccessor.java create mode 100644 src/main/java/com/paulhammant/ngwebdriver/ByAngularBinding.java create mode 100644 src/main/java/com/paulhammant/ngwebdriver/ByAngularRepeater.java create mode 100644 src/main/java/com/paulhammant/ngwebdriver/ByAngularRepeaterCell.java create mode 100644 src/main/java/com/paulhammant/ngwebdriver/ByAngularRepeaterColumn.java create mode 100644 src/main/java/com/paulhammant/ngwebdriver/ByAngularRepeaterRow.java create mode 100644 src/main/java/com/paulhammant/ngwebdriver/WaitForAngularRequestsToFinish.java create mode 100644 src/test/java/com/paulhammant/ngwebdriver/AngularAndWebDriverTest.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..090dfc0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +target +*.iml +*.ipr +*.iws +.idea \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9efb04c --- /dev/null +++ b/LICENSE @@ -0,0 +1,23 @@ +The MIT License + +Portions Copyright (c) 2010-2013 Google, Inc. +Portions Copyright (c) 2013 Paul Hammant + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..5990d84 --- /dev/null +++ b/pom.xml @@ -0,0 +1,49 @@ + + 4.0.0 + + com.paulhammant + ngwebdriver + 1.0-SNAPSHOT + jar + + ngWebDriver + + + UTF-8 + + + + + org.seleniumhq.selenium + selenium-api + 2.0-SNAPSHOT + provided + + + org.testng + testng + 6.8.5 + test + + + junit + junit + + + + + org.hamcrest + hamcrest-all + 1.3 + test + + + org.seleniumhq.selenium + selenium-firefox-driver + 2.0-SNAPSHOT + + test + + + diff --git a/src/main/java/com/paulhammant/ngwebdriver/AngularModelAccessor.java b/src/main/java/com/paulhammant/ngwebdriver/AngularModelAccessor.java new file mode 100644 index 0000000..555ffbd --- /dev/null +++ b/src/main/java/com/paulhammant/ngwebdriver/AngularModelAccessor.java @@ -0,0 +1,42 @@ +package com.paulhammant.ngwebdriver; + +import org.openqa.selenium.JavascriptExecutor; +import org.openqa.selenium.WebElement; + +public class AngularModelAccessor { + + + private JavascriptExecutor driver; + + public AngularModelAccessor(JavascriptExecutor driver) { + this.driver = driver; + } + + public void mutate(WebElement element, final String variable, final String value) { + + driver.executeScript("angular.element(arguments[0]).scope()." + variable + " = " + value + ";" + + "angular.element(document.body).injector().get('$rootScope').$apply();", element); + + + } + + public String retrieveJson(WebElement element, final String variable) { + + return (String) driver.executeScript("return angular.toJson(angular.element(arguments[0]).scope()." + variable + ");", element); + + } + + public Object retrieve(WebElement element, final String variable) { + return driver.executeScript("return angular.element(arguments[0]).scope()." + variable + ";", element); + } + + public String retrieveAsString(WebElement element, final String variable) { + return (String) driver.executeScript("return angular.element(arguments[0]).scope()." + variable + ";", element); + } + + public Long retrieveAsLong(WebElement element, final String variable) { + return (Long) driver.executeScript("return angular.element(arguments[0]).scope()." + variable + ";", element); + } + + +} diff --git a/src/main/java/com/paulhammant/ngwebdriver/ByAngularBinding.java b/src/main/java/com/paulhammant/ngwebdriver/ByAngularBinding.java new file mode 100644 index 0000000..830b41a --- /dev/null +++ b/src/main/java/com/paulhammant/ngwebdriver/ByAngularBinding.java @@ -0,0 +1,49 @@ +package com.paulhammant.ngwebdriver; + +import org.openqa.selenium.By; +import org.openqa.selenium.SearchContext; +import org.openqa.selenium.WebElement; + +import java.util.List; + +public class ByAngularBinding extends By { + + public static ByAngularBinding angularBinding(String binding) { + return new ByAngularBinding(binding); + } + + private String binding; + private int row = 1; + + public ByAngularBinding(String binding) { + this.binding = binding; + } + + private By makeJsBy(String oneOrAll) { + return By.js( + "var using = arguments[0] || document;\n" + + "var binding = '" + binding + "';\n" + + "var bindings = using.getElementsByClassName('ng-binding');\n" + + "var matches = [];\n" + + "for (var i = 0; i < bindings.length; ++i) {\n" + + " var bindingName = angular.element(bindings[i]).data().$binding[0].exp ||\n" + + " angular.element(bindings[i]).data().$binding;\n" + + " if (bindingName.indexOf(binding) != -1) {\n" + + " matches.push(bindings[i]);\n" + + " }\n" + + "}\n" + + "return matches" + oneOrAll + ";" + ); + } + + @Override + public WebElement findElement(SearchContext context) { + return makeJsBy("[0]").findElement(context); + } + + @Override + public List findElements(SearchContext searchContext) { + return makeJsBy("").findElements(searchContext); + } + +} diff --git a/src/main/java/com/paulhammant/ngwebdriver/ByAngularRepeater.java b/src/main/java/com/paulhammant/ngwebdriver/ByAngularRepeater.java new file mode 100644 index 0000000..9a4cc60 --- /dev/null +++ b/src/main/java/com/paulhammant/ngwebdriver/ByAngularRepeater.java @@ -0,0 +1,59 @@ +package com.paulhammant.ngwebdriver; + +import org.openqa.selenium.By; +import org.openqa.selenium.SearchContext; +import org.openqa.selenium.WebElement; + +import java.util.List; + +public class ByAngularRepeater extends By { + + public static ByAngularRepeater angularRepeater(String repeater) { + return new ByAngularRepeater(repeater); + } + + private String repeater; + + public ByAngularRepeater(String repeater) { + this.repeater = repeater; + } + + private By makeJsBy() { + return By.js( + "var using = arguments[0] || document;\n" + + "var repeater = '" + repeater + "';\n" + + "\n" + + "var rows = [];\n" + + "var prefixes = ['ng-', 'ng_', 'data-ng-', 'x-ng-'];\n" + + "for (var p = 0; p < prefixes.length; ++p) {\n" + + " var attr = prefixes[p] + 'repeat';\n" + + " var repeatElems = using.querySelectorAll('[' + attr + ']');\n" + + " attr = attr.replace(/\\\\/g, '');\n" + + " for (var i = 0; i < repeatElems.length; ++i) {\n" + + " if (repeatElems[i].getAttribute(attr).indexOf(repeater) != -1) {\n" + + " rows.push(repeatElems[i]);\n" + + " }\n" + + " }\n" + + "}\n" + + "return rows;"); + } + + public ByAngularRepeaterRow row(int row) { + return new ByAngularRepeaterRow(repeater, row); + } + + public ByAngularRepeaterColumn column(String column) { + return new ByAngularRepeaterColumn(repeater, column); + } + + @Override + public WebElement findElement(SearchContext context) { + return makeJsBy().findElement(context); + } + + @Override + public List findElements(SearchContext searchContext) { + return makeJsBy().findElements(searchContext); + } + +} diff --git a/src/main/java/com/paulhammant/ngwebdriver/ByAngularRepeaterCell.java b/src/main/java/com/paulhammant/ngwebdriver/ByAngularRepeaterCell.java new file mode 100644 index 0000000..1a272a8 --- /dev/null +++ b/src/main/java/com/paulhammant/ngwebdriver/ByAngularRepeaterCell.java @@ -0,0 +1,72 @@ +package com.paulhammant.ngwebdriver; + +import org.openqa.selenium.By; +import org.openqa.selenium.SearchContext; +import org.openqa.selenium.WebElement; + +import java.util.List; + +public class ByAngularRepeaterCell extends By { + + private final String repeater; + private final int row; + private final String column; + + public ByAngularRepeaterCell(String repeater, int row, String column) { + this.repeater = repeater; + this.row = row; + this.column = column; + } + + private By makeJsBy() { + return By.js( + "var matches = [];\n" + + "var using = arguments[0] || document;\n" + + "var repeater = '" + repeater + "';\n" + + "var index = " + row + ";\n" + + "var binding = '" + column + "';\n" + + "var rows = [];\n" + + "var prefixes = ['ng-', 'ng_', 'data-ng-', 'x-ng-', 'ng\\\\:'];\n" + + "for (var p = 0; p < prefixes.length; ++p) {\n" + + " var attr = prefixes[p] + 'repeat';\n" + + " var repeatElems = using.querySelectorAll('[' + attr + ']');\n" + + " attr = attr.replace(/\\\\/g, '');\n" + + " for (var i = 0; i < repeatElems.length; ++i) {\n" + + " if (repeatElems[i].getAttribute(attr).indexOf(repeater) != -1) {\n" + + " rows.push(repeatElems[i]);\n" + + " }\n" + + " }\n" + + "}\n" + + "var row = rows[index - 1];\n" + + "var bindings = [];\n" + + "if (row.className.indexOf('ng-binding') != -1) {\n" + + " bindings.push(row);\n" + + "}\n" + + "var childBindings = row.getElementsByClassName('ng-binding');\n" + + "for (var i = 0; i < childBindings.length; ++i) {\n" + + " bindings.push(childBindings[i]);\n" + + "}\n" + + "for (var i = 0; i < bindings.length; ++i) {\n" + + " var bindingName = angular.element(bindings[i]).data().$binding[0].exp ||\n" + + " angular.element(bindings[i]).data().$binding;\n" + + " if (bindingName.indexOf(binding) != -1) {\n" + + " matches.push(bindings[i]);\n" + + " }\n" + + "}\n" + + "// We can only return one with webdriver.findElement.\n" + + "return matches[0];" + ); + } + + @Override + public WebElement findElement(SearchContext context) { + return makeJsBy().findElement(context); + } + + // meaningless + @Override + public List findElements(SearchContext searchContext) { + throw new UnsupportedOperationException("This locator zooms in on a single row, findElements() is meaningless"); + } + +} diff --git a/src/main/java/com/paulhammant/ngwebdriver/ByAngularRepeaterColumn.java b/src/main/java/com/paulhammant/ngwebdriver/ByAngularRepeaterColumn.java new file mode 100644 index 0000000..a06021c --- /dev/null +++ b/src/main/java/com/paulhammant/ngwebdriver/ByAngularRepeaterColumn.java @@ -0,0 +1,71 @@ +package com.paulhammant.ngwebdriver; + +import org.openqa.selenium.By; +import org.openqa.selenium.SearchContext; +import org.openqa.selenium.WebElement; + +import java.util.List; + +public class ByAngularRepeaterColumn extends By { + + private final String repeater; + private final String column; + + public ByAngularRepeaterColumn(String repeater, String column) { + this.repeater = repeater; + this.column = column; + } + + private By makeJsBy() { + return By.js( + "var matches = [];\n" + + "var using = arguments[0] || document;\n" + + "var repeater = '" + repeater + "';\n" + + "var binding = '" + column + "';\n" + + "\n" + + "var rows = [];\n" + + "var prefixes = ['ng-', 'ng_', 'data-ng-', 'x-ng-'];\n" + + "for (var p = 0; p < prefixes.length; ++p) {\n" + + " var attr = prefixes[p] + 'repeat';\n" + + " var repeatElems = using.querySelectorAll('[' + attr + ']');\n" + + " attr = attr.replace(/\\\\/g, '');\n" + + " for (var i = 0; i < repeatElems.length; ++i) {\n" + + " if (repeatElems[i].getAttribute(attr).indexOf(repeater) != -1) {\n" + + " rows.push(repeatElems[i]);\n" + + " }\n" + + " }\n" + + "}\n" + + "for (var i = 0; i < rows.length; ++i) {\n" + + " var bindings = [];\n" + + " if (rows[i].className.indexOf('ng-binding') != -1) {\n" + + " bindings.push(rows[i]);\n" + + " }\n" + + " var childBindings = rows[i].getElementsByClassName('ng-binding');\n" + + " for (var k = 0; k < childBindings.length; ++k) {\n" + + " bindings.push(childBindings[k]);\n" + + " }\n" + + " for (var j = 0; j < bindings.length; ++j) {\n" + + " var bindingName = angular.element(bindings[j]).data().$binding[0].exp ||\n" + + " angular.element(bindings[j]).data().$binding;\n" + + " if (bindingName.indexOf(binding) != -1) {\n" + + " matches.push(bindings[j]);\n" + + " }\n" + + " }\n" + + "}\n" + + "return matches;" + ); + } + + // meaningless + @Override + public WebElement findElement(SearchContext context) { + throw new UnsupportedOperationException("This locator zooms in on a multiple cells, findElement() is meaningless"); + } + + @Override + public List findElements(SearchContext searchContext) { + return makeJsBy().findElements(searchContext); + + } + +} diff --git a/src/main/java/com/paulhammant/ngwebdriver/ByAngularRepeaterRow.java b/src/main/java/com/paulhammant/ngwebdriver/ByAngularRepeaterRow.java new file mode 100644 index 0000000..3ffc0e9 --- /dev/null +++ b/src/main/java/com/paulhammant/ngwebdriver/ByAngularRepeaterRow.java @@ -0,0 +1,56 @@ +package com.paulhammant.ngwebdriver; + +import org.openqa.selenium.By; +import org.openqa.selenium.SearchContext; +import org.openqa.selenium.WebElement; + +import java.util.List; + +public class ByAngularRepeaterRow extends By { + + private final String repeater; + private final int row; + + public ByAngularRepeaterRow(String repeater, int row) { + this.repeater = repeater; + this.row = row; + } + + private By makeJsBy() { + return By.js( + "var using = arguments[0] || document;\n" + + "var repeater = '" + repeater + "';\n" + + "var index = " + row + ";\n" + + "\n" + + "var rows = [];\n" + + "var prefixes = ['ng-', 'ng_', 'data-ng-', 'x-ng-'];\n" + + "for (var p = 0; p < prefixes.length; ++p) {\n" + + " var attr = prefixes[p] + 'repeat';\n" + + " var repeatElems = using.querySelectorAll('[' + attr + ']');\n" + + " attr = attr.replace(/\\\\/g, '');\n" + + " for (var i = 0; i < repeatElems.length; ++i) {\n" + + " if (repeatElems[i].getAttribute(attr).indexOf(repeater) != -1) {\n" + + " rows.push(repeatElems[i]);\n" + + " }\n" + + " }\n" + + "}\n" + + "return rows[index - 1];"); + } + + public ByAngularRepeaterCell column(String column) { + return new ByAngularRepeaterCell(repeater, row, column); + } + + + @Override + public WebElement findElement(SearchContext context) { + return makeJsBy().findElement(context); + } + + // meaningless + @Override + public List findElements(SearchContext searchContext) { + throw new UnsupportedOperationException("This locator zooms in on a single row, findElements() is meaningless"); + } + +} diff --git a/src/main/java/com/paulhammant/ngwebdriver/WaitForAngularRequestsToFinish.java b/src/main/java/com/paulhammant/ngwebdriver/WaitForAngularRequestsToFinish.java new file mode 100644 index 0000000..ddfba6b --- /dev/null +++ b/src/main/java/com/paulhammant/ngwebdriver/WaitForAngularRequestsToFinish.java @@ -0,0 +1,11 @@ +package com.paulhammant.ngwebdriver; + +import org.openqa.selenium.JavascriptExecutor; + +public class WaitForAngularRequestsToFinish { + + public static void waitForAngularRequestsToFinish(JavascriptExecutor driver) { + driver.executeAsyncScript("var callback = arguments[arguments.length - 1];" + + "angular.element(document.body).injector().get('$browser').notifyWhenNoOutstandingRequests(callback);"); + } +} diff --git a/src/test/java/com/paulhammant/ngwebdriver/AngularAndWebDriverTest.java b/src/test/java/com/paulhammant/ngwebdriver/AngularAndWebDriverTest.java new file mode 100644 index 0000000..804b850 --- /dev/null +++ b/src/test/java/com/paulhammant/ngwebdriver/AngularAndWebDriverTest.java @@ -0,0 +1,169 @@ +package com.paulhammant.ngwebdriver; + +import org.openqa.selenium.WebElement; +import org.openqa.selenium.firefox.FirefoxDriver; +import org.testng.annotations.AfterTest; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import static com.paulhammant.ngwebdriver.ByAngularBinding.angularBinding; +import static com.paulhammant.ngwebdriver.ByAngularRepeater.angularRepeater; +import static com.paulhammant.ngwebdriver.WaitForAngularRequestsToFinish.waitForAngularRequestsToFinish; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.openqa.selenium.By.className; +import static org.openqa.selenium.By.tagName; + +public class AngularAndWebDriverTest { + + private FirefoxDriver driver; + + @BeforeTest + public void setup() { + driver = new FirefoxDriver(); + driver.manage().timeouts().setScriptTimeout(30, TimeUnit.SECONDS); + driver.get("https://online.jimmyjohns.com/#/pickupresults/58"); + waitForAngularRequestsToFinish(driver); + } + + @AfterTest + public void tear_down() { + driver.quit(); + } + + @Test + public void find_ng_repeat_in_page() { + + // find the second address + List wes = driver.findElements(angularRepeater("location in Locations")); + + assertThat(wes.size(), is(3)); + assertThat(wes.get(0).findElement(className("addressContent")).getText(), containsString("Chicago, IL")); + assertThat(wes.get(1).findElement(className("addressContent")).getText(), containsString("Chicago, IL")); + assertThat(wes.get(2).findElement(className("addressContent")).getText(), containsString("Chicago, IL")); + + + } + + @Test + public void find_second_row_in_ng_repeat() { + + // find the second address + WebElement we = driver.findElement(angularRepeater("location in Locations").row(2)) + .findElement(className("addressContent")); + + assertThat(cleanup(we), is( + "3328 N Clark St\n" + + "Chicago, IL\n" + + "773-244-9000\n" + + "min order $3.75" + )); + + } + + private String cleanup(WebElement we) { + return we.getText().replace("Today's hours: 10:30 am - 10 pm\n", ""); + } + + @Test + public void find_third_row_in_ng_repeat_by_default_from_intermediate_node() { + + WebElement we = driver.findElement(tagName("body")) + .findElement(angularRepeater("location in Locations").row(3)) + .findElement(className("addressContent")); + + assertThat(cleanup(we), is( + "46 E Chicago Ave\n" + + "Chicago, IL\n" + + "312-787-0100\n" + + "min order $3.00" + )); + } + + @Test + public void find_specific_cell_in_ng_repeat() { + + // find the second address + WebElement we = driver.findElement(angularRepeater("location in Locations").row(2).column("location.City")); + + assertThat(we.getText(), is("Chicago, IL")); + } + + @Test + public void find_all_of_a_coumn_in_an_ng_repeat() { + + // find all the telephone numbers + List we = driver.findElements(angularRepeater("location in Locations").column("location.Phone")); + + assertThat(we.get(0).getText(), is("312-733-8030")); + assertThat(we.get(1).getText(), is("773-244-9000")); + assertThat(we.get(2).getText(), is("312-787-0100")); + } + + @Test + public void find_by_angular_binding() { + + // find the first telephone number + WebElement we = driver.findElement(angularBinding("location.Phone")); + // could have been {{location.Phone}} too, or even ion.Pho + + assertThat(we.getText(), is("312-733-8030")); + } + + @Test + public void find_all_for_an_angular_binding() { + + // find all the telephone numbers + List wes = driver.findElements(angularBinding("location.Phone")); + + assertThat(wes.get(0).getText(), is("312-733-8030")); + assertThat(wes.get(1).getText(), is("773-244-9000")); + assertThat(wes.get(2).getText(), is("312-787-0100")); + + } + + @Test + public void model_mutation_and_query_is_possible() { + + WebElement we = driver.findElement(className("addressContent")); + + assertThat(we.getText(), containsString("812 W Van Buren St\nChicago, IL")); + + AngularModelAccessor model = new AngularModelAccessor(driver); + model.mutate(we, "location.City", "'Narnia'"); + + assertThat(we.getText(), containsString("812 W Van Buren St\nNarnia, IL")); + + String locn = model.retrieveJson(we, "location"); + + assertThat(locn.replace("\"","'"), containsString("{'Id':1675,'Name':'#0019 812 W Van Buren St','Abbreviation':'#0019'")); + + String city = model.retrieveJson(we, "location.City"); + + assertThat(city, is("\"Narnia\"")); + + city = model.retrieveAsString(we, "location.City"); + + assertThat(city, is("Narnia")); + + Object rv = model.retrieve(we, "location.City"); + + assertThat(rv.toString(), is("Narnia")); + + rv = model.retrieve(we, "location"); + + assertThat(((Map) rv).get("City").toString(), is("Narnia")); + + Long id = model.retrieveAsLong(we, "location.Id"); + + assertThat(id, is(1675L)); + + } + + +}