├── .gitignore ├── AngularJSLibrary ├── __init__.py ├── angular.robot ├── angular_wait.robot ├── async.html ├── async.js ├── demo_ngcdk_dialog.robot ├── demo_phonecat.robot ├── demo_waitforangular.robot ├── ng-repeater.js ├── ng-repeater.min.js ├── test │ ├── testExecAsyncWithCallback.py │ └── testWaitForAngular.py ├── test_angular2.robot └── testserver.py.patch ├── CHANGES.rst ├── DEVELOPERS.rst ├── LICENSE ├── README.rst ├── TESTING.rst ├── docs └── index.html └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | 59 | # Emacs 60 | \#*\# 61 | *.*~ -------------------------------------------------------------------------------- /AngularJSLibrary/__init__.py: -------------------------------------------------------------------------------- 1 | from robot.api import logger 2 | from robot.libraries.BuiltIn import BuiltIn, RobotNotRunningError 3 | from robot.utils import timestr_to_secs 4 | from selenium.webdriver.support.ui import WebDriverWait 5 | from selenium.common.exceptions import TimeoutException 6 | from SeleniumLibrary.locators import ElementFinder 7 | 8 | try: 9 | from exceptions import AttributeError 10 | except ImportError: 11 | pass 12 | import time 13 | 14 | js_wait_for_angularjs = """ 15 | var waiting = true; 16 | var callback = function () {waiting = false;} 17 | var el = document.querySelector('[ng-app]'); 18 | if (typeof angular.element(el).injector() == "undefined") { 19 | throw new Error('root element ([ng-app]) has no injector.' + 20 | ' this may mean it is not inside ng-app.'); 21 | } 22 | angular.element(el).injector().get('$browser'). 23 | notifyWhenNoOutstandingRequests(callback); 24 | return waiting; 25 | """ 26 | 27 | js_wait_for_angular = """ 28 | var waiting = true; 29 | var callback = function () {waiting = false;} 30 | var el = document.querySelector(arguments[0]); 31 | if (!el) { 32 | throw new Error('Unable to find root selector that is given by importing the library using "' + 33 | arguments[0] + 34 | '". Please refer to the AngularJS library documentation' + 35 | ' for more information on how to resolve this error.') 36 | } 37 | if (window.angular && !(window.angular.version && 38 | window.angular.version.major > 1)) { 39 | /* ng1 */ 40 | angular.element(el).injector().get('$browser'). 41 | notifyWhenNoOutstandingRequests(callback); 42 | } else if (window.getAngularTestability) { 43 | return !window.getAngularTestability(el).isStable(callback); 44 | } else if (window.getAllAngularTestabilities) { 45 | throw new Error('AngularJSLibrary does not currently handle ' + 46 | 'window.getAllAngularTestabilities. It does work on sites supporting ' + 47 | 'window.getAngularTestability. If you require this functionality, please ' + 48 | 'the library authors or reach out to the Robot Framework Users Group.'); 49 | } else if (!window.angular) { 50 | throw new Error('window.angular is undefined. This could be either ' + 51 | 'because this is a non-angular page or because your test involves ' + 52 | 'client-side navigation. Currently the AngularJS Library is not ' + 53 | 'designed to wait in such situations. Instead you should explicitly ' + 54 | 'call the "Wait For Angular" keyword.'); 55 | } else if (window.angular.version >= 2) { 56 | throw new Error('You appear to be using angular, but window.' + 57 | 'getAngularTestability was never set. This may be due to bad ' + 58 | 'obfuscation.'); 59 | } else { 60 | throw new Error('Cannot get testability API for unknown angular ' + 61 | 'version "' + window.angular.version + '"'); 62 | } 63 | return waiting; 64 | """ 65 | 66 | 67 | js_get_pending_http_requests=""" 68 | var el = document.querySelector('[ng-app]'); 69 | var $injector = angular.element(el).injector(); 70 | var $http = $injector.get('$http'); 71 | return $http.pendingRequests; 72 | """ 73 | 74 | js_repeater_min = """ 75 | var rootSelector=null;function byRepeaterInner(b){var a="by."+(b?"exactR":"r")+"epeater";return function(c){return{getElements:function(d){return findAllRepeaterRows(c,b,d)},row:function(d){return{getElements:function(e){return findRepeaterRows(c,b,d,e)},column:function(e){return{getElements:function(f){return findRepeaterElement(c,b,d,e,f,rootSelector)}}}}},column:function(d){return{getElements:function(e){return findRepeaterColumn(c,b,d,e,rootSelector)},row:function(e){return{getElements:function(f){return findRepeaterElement(c,b,e,d,f,rootSelector)}}}}}}}}repeater=byRepeaterInner(false);exactRepeater=byRepeaterInner(true);function repeaterMatch(a,b,c){if(c){return a.split(" track by ")[0].split(" as ")[0].split("|")[0].split("=")[0].trim()==b}else{return a.indexOf(b)!=-1}}function findRepeaterRows(k,e,g,l){l=l||document;var d=["ng-","ng_","data-ng-","x-ng-",arguments[1]];var o=[];for(var a=0;a 3 |
  • 4 | 5 | 6 |
  • 7 |
  • 8 | 9 | 10 |
  • 11 |
  • 12 | 13 | 14 |
  • 15 |
  • 16 | 17 | 18 | 19 |
  • 20 |
  • 21 | 22 | 23 | 24 |
  • 25 |
  • 26 | 27 | 28 | 29 |
  • 30 |
  • 31 | 32 | 33 |
  • 34 |
  • 35 | 36 |
    37 |
  • 38 | 39 | -------------------------------------------------------------------------------- /AngularJSLibrary/async.js: -------------------------------------------------------------------------------- 1 | function AsyncCtrl($scope, $http, $timeout, $location) { 2 | $scope.slowHttpStatus = 'not started'; 3 | $scope.slowFunctionStatus = 'not started'; 4 | $scope.slowTimeoutStatus = 'not started'; 5 | $scope.slowAngularTimeoutStatus = 'not started'; 6 | $scope.slowAngularTimeoutCompleted = false; 7 | $scope.slowAngularTimeoutPromiseStatus = 'not started'; 8 | $scope.slowAngularTimeoutPromiseCompleted = false; 9 | $scope.slowHttpPromiseStatus = 'not started'; 10 | $scope.slowHttpPromiseCompleted = false; 11 | $scope.routingChangeStatus = 'not started'; 12 | $scope.templateUrl = 'fastTemplateUrl'; 13 | 14 | $scope.slowHttp = function() { 15 | $scope.slowHttpStatus = 'pending...'; 16 | $http({method: 'GET', url: 'slowcall'}).success(function() { 17 | $scope.slowHttpStatus = 'done'; 18 | }); 19 | }; 20 | 21 | $scope.slowFunction = function() { 22 | $scope.slowFunctionStatus = 'pending...'; 23 | for (var i = 0, t = 0; i < 500000000; ++i) { 24 | t++; 25 | } 26 | $scope.slowFunctionStatus = 'done'; 27 | }; 28 | 29 | $scope.slowTimeout = function() { 30 | $scope.slowTimeoutStatus = 'pending...'; 31 | window.setTimeout(function() { 32 | $scope.$apply(function() { 33 | $scope.slowTimeoutStatus = 'done'; 34 | }); 35 | }, 5000); 36 | }; 37 | 38 | $scope.slowAngularTimeout = function() { 39 | $scope.slowAngularTimeoutStatus = 'pending...'; 40 | $timeout(function() { 41 | $scope.slowAngularTimeoutStatus = 'done'; 42 | $scope.slowAngularTimeoutCompleted = true; 43 | }, 4000); 44 | }; 45 | 46 | $scope.slowAngularTimeoutHideButton = function() { 47 | $scope.slowAngularTimeoutCompleted = false; 48 | }; 49 | 50 | $scope.slowAngularTimeoutPromise = function() { 51 | $scope.slowAngularTimeoutPromiseStatus = 'pending...'; 52 | $timeout(function() { 53 | // intentionally empty 54 | }, 4000).then(function() { 55 | $scope.slowAngularTimeoutPromiseStatus = 'done'; 56 | $scope.slowAngularTimeoutPromiseCompleted = true; 57 | }); 58 | }; 59 | 60 | $scope.slowAngularTimeoutPromiseHideButton = function() { 61 | $scope.slowAngularTimeoutPromiseCompleted = false; 62 | }; 63 | 64 | $scope.slowHttpPromise = function() { 65 | $scope.slowHttpPromiseStatus = 'pending...'; 66 | $http({method: 'GET', url: 'slowcall'}).success(function() { 67 | // intentionally empty 68 | }).then(function() { 69 | $scope.slowHttpPromiseStatus = 'done'; 70 | $scope.slowHttpPromiseCompleted = true; 71 | }); 72 | }; 73 | 74 | $scope.slowHttpPromiseHideButton = function() { 75 | $scope.slowHttpPromiseCompleted = false; 76 | }; 77 | 78 | $scope.routingChange = function() { 79 | $scope.routingChangeStatus = 'pending...'; 80 | $location.url('slowloader'); 81 | }; 82 | 83 | $scope.changeTemplateUrl = function() { 84 | $scope.templateUrl = 'slowTemplateUrl'; 85 | }; 86 | } 87 | 88 | AsyncCtrl.$inject = ['$scope', '$http', '$timeout', '$location']; 89 | 90 | angular.module('myApp.appVersion', []). 91 | value('version', '0.1-robotframework-angularjs-031618'). 92 | directive('appVersion', ['version', function(version) { 93 | return function(scope, elm, attrs) { 94 | elm.text(version); 95 | }; 96 | }]); 97 | -------------------------------------------------------------------------------- /AngularJSLibrary/demo_ngcdk_dialog.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Library SeleniumLibrary 3 | Library AngularJSLibrary root_selector=material-docs-app 4 | 5 | *** Test Cases *** 6 | Add Favorite Animal To Dialog 7 | Open Browser https://material.angular.io/cdk/dialog/examples Chrome 8 | Input Text //input[@for='dialog-user-name'] Robot Framework 9 | Click Button Pick one 10 | Input Text //input[@for='favorite-animal'] Aibo 11 | Click Button OK 12 | Element Text Should Be //cdk-dialog-overview-example/ol/li[3] You chose: Aibo -------------------------------------------------------------------------------- /AngularJSLibrary/demo_phonecat.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Library SeleniumLibrary 3 | Library AngularJSLibrary root_selector=[ng-app] 4 | 5 | *** Test Cases *** 6 | Search Through The Phone Catalog For Samsung Phones 7 | Open Browser http://angular.github.io/angular-phonecat/step-14/app Chrome 8 | Input Text //input Samsung 9 | Click Link Samsung Galaxy Tab™ 10 | Element Text Should Be css:phone-detail h1 Samsung Galaxy Tab™ -------------------------------------------------------------------------------- /AngularJSLibrary/demo_waitforangular.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Library SeleniumLibrary 3 | Library AngularJSLibrary ignore_implicit_angular_wait=True 4 | 5 | *** Test Cases *** 6 | Search Through The Phone Catalog For Samsung Phones 7 | Open Browser http://angular.github.io/angular-phonecat/step-14/app Chrome 8 | Wait For Angular 9 | Input Text //input Samsung 10 | Wait For Angular 11 | Click Link Samsung Galaxy Tab™ 12 | Wait For Angular 13 | Element Text Should Be css:phone-detail h1 Samsung Galaxy Tab™ -------------------------------------------------------------------------------- /AngularJSLibrary/ng-repeater.js: -------------------------------------------------------------------------------- 1 | var rootSelector = null; 2 | 3 | // Generate either by.repeater or by.exactRepeater 4 | function byRepeaterInner(exact) { 5 | var name = 'by.' + (exact ? 'exactR' : 'r') + 'epeater'; 6 | return function(repeatDescriptor) { 7 | return { 8 | getElements: function(using) { 9 | return findAllRepeaterRows(repeatDescriptor, exact, using); 10 | }, 11 | //toString: function toString() { 12 | // return name + '("' + repeatDescriptor + '")'; 13 | //}, 14 | row: function(index) { 15 | return { 16 | getElements: function(using) { 17 | return findRepeaterRows(repeatDescriptor, exact, index, using); 18 | }, 19 | //toString: function toString() { 20 | // return name + '(' + repeatDescriptor + '").row("' + index + '")"'; 21 | //}, 22 | column: function(binding) { 23 | return { 24 | getElements: function(using) { 25 | return findRepeaterElement(repeatDescriptor, exact, index, binding, using, rootSelector); 26 | }//, 27 | //toString: function toString() { 28 | // return name + '("' + repeatDescriptor + '").row("' + index + 29 | // '").column("' + binding + '")'; 30 | //} 31 | }; 32 | } 33 | }; 34 | }, 35 | column: function(binding) { 36 | return { 37 | getElements: function(using) { 38 | return findRepeaterColumn(repeatDescriptor, exact, binding, using, rootSelector); 39 | }, 40 | //toString: function toString() { 41 | // return name + '("' + repeatDescriptor + '").column("' + 42 | // binding + '")'; 43 | //}, 44 | row: function(index) { 45 | return { 46 | getElements: function (using) { 47 | return findRepeaterElement(repeatDescriptor, exact, index, binding, using, rootSelector); 48 | }//, 49 | //toString: function toString() { 50 | // return name + '("' + repeatDescriptor + '").column("' + 51 | // binding + '").row("' + index + '")'; 52 | //} 53 | }; 54 | } 55 | }; 56 | } 57 | }; 58 | }; 59 | } 60 | 61 | /** 62 | * Find elements inside an ng-repeat. 63 | * 64 | * @view 65 | *
    66 | * {{cat.name}} 67 | * {{cat.age}} 68 | *
    69 | * 70 | *
    71 | * {{$index}} 72 | *
    73 | *
    74 | *

    {{book.name}}

    75 | *

    {{book.blurb}}

    76 | *
    77 | * 78 | * @example 79 | * // Returns the DIV for the second cat. 80 | * var secondCat = element(by.repeater('cat in pets').row(1)); 81 | * 82 | * // Returns the SPAN for the first cat's name. 83 | * var firstCatName = element(by.repeater('cat in pets'). 84 | * row(0).column('cat.name')); 85 | * 86 | * // Returns a promise that resolves to an array of WebElements from a column 87 | * var ages = element.all( 88 | * by.repeater('cat in pets').column('cat.age')); 89 | * 90 | * // Returns a promise that resolves to an array of WebElements containing 91 | * // all top level elements repeated by the repeater. For 2 pets rows resolves 92 | * // to an array of 2 elements. 93 | * var rows = element.all(by.repeater('cat in pets')); 94 | * 95 | * // Returns a promise that resolves to an array of WebElements containing all 96 | * // the elements with a binding to the book's name. 97 | * var divs = element.all(by.repeater('book in library').column('book.name')); 98 | * 99 | * // Returns a promise that resolves to an array of WebElements containing 100 | * // the DIVs for the second book. 101 | * var bookInfo = element.all(by.repeater('book in library').row(1)); 102 | * 103 | * // Returns the H4 for the first book's name. 104 | * var firstBookName = element(by.repeater('book in library'). 105 | * row(0).column('book.name')); 106 | * 107 | * // Returns a promise that resolves to an array of WebElements containing 108 | * // all top level elements repeated by the repeater. For 2 books divs 109 | * // resolves to an array of 4 elements. 110 | * var divs = element.all(by.repeater('book in library')); 111 | * 112 | * @param {string} repeatDescriptor 113 | * @return {{findElementsOverride: findElementsOverride, toString: Function|string}} 114 | */ 115 | repeater = byRepeaterInner(false); 116 | 117 | /** 118 | * Find an element by exact repeater. 119 | * 120 | * @view 121 | *
  • 122 | *
  • 123 | * 124 | * @example 125 | * expect(element(by.exactRepeater('person in peopleWithRedHair')).isPresent()) 126 | * .toBe(true); 127 | * expect(element(by.exactRepeater('person in people')).isPresent()).toBe(false); 128 | * expect(element(by.exactRepeater('car in cars')).isPresent()).toBe(true); 129 | * 130 | * @param {string} repeatDescriptor 131 | * @return {{findElementsOverride: findElementsOverride, toString: Function|string}} 132 | */ 133 | exactRepeater = byRepeaterInner(true); 134 | 135 | /* 136 | var functions = {}; 137 | 138 | function wrapWithHelpers(fun) { 139 | var helpers = Array.prototype.slice.call(arguments, 1); 140 | if (!helpers.length) { 141 | return fun; 142 | } 143 | var FunClass = Function; // Get the linter to allow this eval 144 | return new FunClass( 145 | helpers.join(';') + String.fromCharCode(59) + 146 | ' return (' + fun.toString() + ').apply(this, arguments);'); 147 | } 148 | */ 149 | 150 | function repeaterMatch(ngRepeat, repeater, exact) { 151 | if (exact) { 152 | return ngRepeat.split(' track by ')[0].split(' as ')[0].split('|')[0]. 153 | split('=')[0].trim() == repeater; 154 | } else { 155 | return ngRepeat.indexOf(repeater) != -1; 156 | } 157 | } 158 | 159 | function findRepeaterRows(repeater, exact, index, using) { 160 | using = using || document; 161 | 162 | var prefixes = ['ng-', 'ng_', 'data-ng-', 'x-ng-', 'ng\\:']; 163 | var rows = []; 164 | for (var p = 0; p < prefixes.length; ++p) { 165 | var attr = prefixes[p] + 'repeat'; 166 | var repeatElems = using.querySelectorAll('[' + attr + ']'); 167 | attr = attr.replace(/\\/g, ''); 168 | for (var i = 0; i < repeatElems.length; ++i) { 169 | if (repeaterMatch(repeatElems[i].getAttribute(attr), repeater, exact)) { 170 | rows.push(repeatElems[i]); 171 | } 172 | } 173 | } 174 | /* multiRows is an array of arrays, where each inner array contains 175 | one row of elements. */ 176 | var multiRows = []; 177 | for (var p = 0; p < prefixes.length; ++p) { 178 | var attr = prefixes[p] + 'repeat-start'; 179 | var repeatElems = using.querySelectorAll('[' + attr + ']'); 180 | attr = attr.replace(/\\/g, ''); 181 | for (var i = 0; i < repeatElems.length; ++i) { 182 | if (repeaterMatch(repeatElems[i].getAttribute(attr), repeater, exact)) { 183 | var elem = repeatElems[i]; 184 | var row = []; 185 | while (elem.nodeType != 8 || 186 | !repeaterMatch(elem.nodeValue, repeater)) { 187 | if (elem.nodeType == 1) { 188 | row.push(elem); 189 | } 190 | elem = elem.nextSibling; 191 | } 192 | multiRows.push(row); 193 | } 194 | } 195 | } 196 | var row = rows[index] || [], multiRow = multiRows[index] || []; 197 | return [].concat(row, multiRow); 198 | } 199 | //functions.findRepeaterRows = wrapWithHelpers(findRepeaterRows, repeaterMatch); 200 | 201 | function findAllRepeaterRows(repeater, exact, using) { 202 | using = using || document; 203 | 204 | var rows = []; 205 | var prefixes = ['ng-', 'ng_', 'data-ng-', 'x-ng-', 'ng\\:']; 206 | for (var p = 0; p < prefixes.length; ++p) { 207 | var attr = prefixes[p] + 'repeat'; 208 | var repeatElems = using.querySelectorAll('[' + attr + ']'); 209 | attr = attr.replace(/\\/g, ''); 210 | for (var i = 0; i < repeatElems.length; ++i) { 211 | if (repeaterMatch(repeatElems[i].getAttribute(attr), repeater, exact)) { 212 | rows.push(repeatElems[i]); 213 | } 214 | } 215 | } 216 | for (var p = 0; p < prefixes.length; ++p) { 217 | var attr = prefixes[p] + 'repeat-start'; 218 | var repeatElems = using.querySelectorAll('[' + attr + ']'); 219 | attr = attr.replace(/\\/g, ''); 220 | for (var i = 0; i < repeatElems.length; ++i) { 221 | if (repeaterMatch(repeatElems[i].getAttribute(attr), repeater, exact)) { 222 | var elem = repeatElems[i]; 223 | while (elem.nodeType != 8 || 224 | !repeaterMatch(elem.nodeValue, repeater)) { 225 | if (elem.nodeType == 1) { 226 | rows.push(elem); 227 | } 228 | elem = elem.nextSibling; 229 | } 230 | } 231 | } 232 | } 233 | return rows; 234 | } 235 | //functions.findAllRepeaterRows = wrapWithHelpers(findAllRepeaterRows, repeaterMatch); 236 | 237 | function findRepeaterElement(repeater, exact, index, binding, using, rootSelector) { 238 | var matches = []; 239 | var root = document.querySelector(rootSelector || 'body'); 240 | using = using || document; 241 | 242 | var rows = []; 243 | var prefixes = ['ng-', 'ng_', 'data-ng-', 'x-ng-', 'ng\\:']; 244 | for (var p = 0; p < prefixes.length; ++p) { 245 | var attr = prefixes[p] + 'repeat'; 246 | var repeatElems = using.querySelectorAll('[' + attr + ']'); 247 | attr = attr.replace(/\\/g, ''); 248 | for (var i = 0; i < repeatElems.length; ++i) { 249 | if (repeaterMatch(repeatElems[i].getAttribute(attr), repeater, exact)) { 250 | rows.push(repeatElems[i]); 251 | } 252 | } 253 | } 254 | /* multiRows is an array of arrays, where each inner array contains 255 | one row of elements. */ 256 | var multiRows = []; 257 | for (var p = 0; p < prefixes.length; ++p) { 258 | var attr = prefixes[p] + 'repeat-start'; 259 | var repeatElems = using.querySelectorAll('[' + attr + ']'); 260 | attr = attr.replace(/\\/g, ''); 261 | for (var i = 0; i < repeatElems.length; ++i) { 262 | if (repeaterMatch(repeatElems[i].getAttribute(attr), repeater, exact)) { 263 | var elem = repeatElems[i]; 264 | var row = []; 265 | while (elem.nodeType != 8 || (elem.nodeValue && 266 | !repeaterMatch(elem.nodeValue, repeater))) { 267 | if (elem.nodeType == 1) { 268 | row.push(elem); 269 | } 270 | elem = elem.nextSibling; 271 | } 272 | multiRows.push(row); 273 | } 274 | } 275 | } 276 | var row = rows[index]; 277 | var multiRow = multiRows[index]; 278 | var bindings = []; 279 | if (row) { 280 | //if (angular.getTestability) { 281 | // matches.push.apply( 282 | // matches, 283 | // angular.getTestability(root).findBindings(row, binding)); 284 | //} else { 285 | if (row.className.indexOf('ng-binding') != -1) { 286 | bindings.push(row); 287 | } 288 | var childBindings = row.getElementsByClassName('ng-binding'); 289 | for (var i = 0; i < childBindings.length; ++i) { 290 | bindings.push(childBindings[i]); 291 | } 292 | //} 293 | } 294 | if (multiRow) { 295 | for (var i = 0; i < multiRow.length; ++i) { 296 | var rowElem = multiRow[i]; 297 | //if (angular.getTestability) { 298 | // matches.push.apply( 299 | // matches, 300 | // angular.getTestability(root).findBindings(rowElem, binding)); 301 | //} else { 302 | if (rowElem.className.indexOf('ng-binding') != -1) { 303 | bindings.push(rowElem); 304 | } 305 | var childBindings = rowElem.getElementsByClassName('ng-binding'); 306 | for (var j = 0; j < childBindings.length; ++j) { 307 | bindings.push(childBindings[j]); 308 | } 309 | //} 310 | } 311 | } 312 | for (var i = 0; i < bindings.length; ++i) { 313 | var dataBinding = angular.element(bindings[i]).data('$binding'); 314 | if (dataBinding) { 315 | var bindingName = dataBinding.exp || dataBinding[0].exp || dataBinding; 316 | if (bindingName.indexOf(binding) != -1) { 317 | matches.push(bindings[i]); 318 | } 319 | } 320 | } 321 | return matches; 322 | } 323 | //functions.findRepeaterElement = wrapWithHelpers(findRepeaterElement, repeaterMatch); 324 | 325 | function findRepeaterColumn(repeater, exact, binding, using, rootSelector) { 326 | var matches = []; 327 | var root = document.querySelector(rootSelector || 'body'); 328 | using = using || document; 329 | 330 | var rows = []; 331 | var prefixes = ['ng-', 'ng_', 'data-ng-', 'x-ng-', 'ng\\:']; 332 | for (var p = 0; p < prefixes.length; ++p) { 333 | var attr = prefixes[p] + 'repeat'; 334 | var repeatElems = using.querySelectorAll('[' + attr + ']'); 335 | attr = attr.replace(/\\/g, ''); 336 | for (var i = 0; i < repeatElems.length; ++i) { 337 | if (repeaterMatch(repeatElems[i].getAttribute(attr), repeater, exact)) { 338 | rows.push(repeatElems[i]); 339 | } 340 | } 341 | } 342 | /* multiRows is an array of arrays, where each inner array contains 343 | one row of elements. */ 344 | var multiRows = []; 345 | for (var p = 0; p < prefixes.length; ++p) { 346 | var attr = prefixes[p] + 'repeat-start'; 347 | var repeatElems = using.querySelectorAll('[' + attr + ']'); 348 | attr = attr.replace(/\\/g, ''); 349 | for (var i = 0; i < repeatElems.length; ++i) { 350 | if (repeaterMatch(repeatElems[i].getAttribute(attr), repeater, exact)) { 351 | var elem = repeatElems[i]; 352 | var row = []; 353 | while (elem.nodeType != 8 || (elem.nodeValue && 354 | !repeaterMatch(elem.nodeValue, repeater))) { 355 | if (elem.nodeType == 1) { 356 | row.push(elem); 357 | } 358 | elem = elem.nextSibling; 359 | } 360 | multiRows.push(row); 361 | } 362 | } 363 | } 364 | var bindings = []; 365 | for (var i = 0; i < rows.length; ++i) { 366 | //if (angular.getTestability) { 367 | // matches.push.apply( 368 | // matches, 369 | // angular.getTestability(root).findBindings(rows[i], binding)); 370 | //} else { 371 | if (rows[i].className.indexOf('ng-binding') != -1) { 372 | bindings.push(rows[i]); 373 | } 374 | var childBindings = rows[i].getElementsByClassName('ng-binding'); 375 | for (var k = 0; k < childBindings.length; ++k) { 376 | bindings.push(childBindings[k]); 377 | } 378 | //} 379 | } 380 | for (var i = 0; i < multiRows.length; ++i) { 381 | for (var j = 0; j < multiRows[i].length; ++j) { 382 | //if (angular.getTestability) { 383 | // matches.push.apply( 384 | // matches, 385 | // angular.getTestability(root).findBindings(multiRows[i][j], binding)); 386 | //} else { 387 | var elem = multiRows[i][j]; 388 | if (elem.className.indexOf('ng-binding') != -1) { 389 | bindings.push(elem); 390 | } 391 | var childBindings = elem.getElementsByClassName('ng-binding'); 392 | for (var k = 0; k < childBindings.length; ++k) { 393 | bindings.push(childBindings[k]); 394 | } 395 | //} 396 | } 397 | } 398 | for (var j = 0; j < bindings.length; ++j) { 399 | var dataBinding = angular.element(bindings[j]).data('$binding'); 400 | if (dataBinding) { 401 | var bindingName = dataBinding.exp || dataBinding[0].exp || dataBinding; 402 | if (bindingName.indexOf(binding) != -1) { 403 | matches.push(bindings[j]); 404 | } 405 | } 406 | } 407 | return matches; 408 | } 409 | //functions.findRepeaterColumn = wrapWithHelpers(findRepeaterColumn, repeaterMatch); 410 | -------------------------------------------------------------------------------- /AngularJSLibrary/ng-repeater.min.js: -------------------------------------------------------------------------------- 1 | var rootSelector=null;function byRepeaterInner(b){var a="by."+(b?"exactR":"r")+"epeater";return function(c){return{getElements:function(d){return findAllRepeaterRows(c,b,d)},row:function(d){return{getElements:function(e){return findRepeaterRows(c,b,d,e)},column:function(e){return{getElements:function(f){return findRepeaterElement(c,b,d,e,f,rootSelector)}}}}},column:function(d){return{getElements:function(e){return findRepeaterColumn(c,b,d,e,rootSelector)},row:function(e){return{getElements:function(f){return findRepeaterElement(c,b,e,d,f,rootSelector)}}}}}}}}repeater=byRepeaterInner(false);exactRepeater=byRepeaterInner(true);function repeaterMatch(a,b,c){if(c){return a.split(" track by ")[0].split(" as ")[0].split("|")[0].split("=")[0].trim()==b}else{return a.indexOf(b)!=-1}}function findRepeaterRows(k,e,g,l){l=l||document;var d=["ng-","ng_","data-ng-","x-ng-","ng\\:"];var o=[];for(var a=0;a World 19 | Click Element //app-root//button 20 | Sleep 5sec 21 | Close All Browsers -------------------------------------------------------------------------------- /AngularJSLibrary/testserver.py.patch: -------------------------------------------------------------------------------- 1 | diff --git a/atest/resources/testserver/testserver.py b/atest/resources/testserver/testserver.py 2 | index 565c650..8e3d82b 100644 3 | --- a/atest/resources/testserver/testserver.py 4 | +++ b/atest/resources/testserver/testserver.py 5 | @@ -3,6 +3,7 @@ 6 | 7 | import os 8 | import sys 9 | +from time import sleep 10 | 11 | from http.client import HTTPConnection 12 | from http.server import SimpleHTTPRequestHandler, HTTPServer 13 | @@ -21,6 +22,36 @@ class StoppableHttpRequestHandler(SimpleHTTPRequestHandler): 14 | def do_POST(self): 15 | self.do_GET() 16 | 17 | + def do_GET(self): 18 | + """Response pages for Angular tests. 19 | + 20 | + Added by AngularJSLibrary 21 | + """ 22 | + if self.path.endswith('/fastcall'): 23 | + self.send_response(200) 24 | + self.send_header('Content-type', 'text/html') 25 | + self.end_headers() 26 | + self.wfile.write('done') 27 | + elif self.path.endswith('/slowcall'): 28 | + sleep(2) 29 | + self.send_response(200) 30 | + self.send_header('Content-type', 'text/html') 31 | + self.end_headers() 32 | + self.wfile.write('finally done') 33 | + elif self.path.endswith('/fastTemplateUrl'): 34 | + self.send_response(200) 35 | + self.send_header('Content-type', 'text/html') 36 | + self.end_headers() 37 | + self.wfile.write(b'fast template contents') 38 | + elif self.path.endswith('/slowTemplateUrl'): 39 | + sleep(2) 40 | + self.send_response(200) 41 | + self.send_header('Content-type', 'text/html') 42 | + self.end_headers() 43 | + self.wfile.write(b'slow template contents') 44 | + else: 45 | + SimpleHTTPRequestHandler.do_GET(self) 46 | + 47 | 48 | class ThreadingHttpServer(ThreadingMixIn, HTTPServer): 49 | pass 50 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 0.0.10 (2019-07-31) 4 | --------- 5 | Changes: 6 | 7 | - Updated library and keyword documentation. 8 | [aaltat][emanlove] 9 | 10 | - Added error and informative message when unable to find root element or root component. 11 | [anthonyfromtheuk][HelioGuilherme66][emanlove] 12 | 13 | - Modified for Python 3 compatibility 14 | [emanlove] 15 | 16 | - Documented discrepancy between the ``Set Ignore Implicit Angular Wait`` keyword argument and the equivalent import library argument. 17 | [HelioGuilherme66][aaltat][emanlove] 18 | 19 | - Update for compatibility with SeleniumLibrary 4.0. 20 | [aaltat][emanlove] 21 | 22 | - Fixed major issue with setup test environment under Windows documentation. 23 | [emanlove] 24 | 25 | 0.0.9 (2018-09-08) 26 | ------------------ 27 | Fixes: 28 | 29 | - Fixed issue when importing library into RIDE. 30 | [pekkaklarck][emanlove] 31 | 32 | 0.0.8 (2018-08-03) 33 | ------------------ 34 | Fixes: 35 | 36 | - Fixed issue when no locator strategy was specified. 37 | [emanlove] 38 | 39 | 0.0.7 (2018-03-31) 40 | ------------------ 41 | Changes: 42 | 43 | - Added support for SeleniumLibrary and dropped support for Selenium2Library. 44 | [emanlove] 45 | 46 | Fixes: 47 | 48 | - [Minor] Corrected error message. 49 | [emanlove] 50 | 51 | 0.0.6 (2017-06-12) 52 | ------------------ 53 | Changes: 54 | 55 | - Allow for setting root selector when importing library. 56 | [emanlove] 57 | 58 | 0.0.5 (2017-06-09) 59 | ------------------ 60 | Changes: 61 | 62 | - Added support for Angular 2 under `Wait For Angular` keyword. 63 | [emanlove] 64 | 65 | - Updated documentation around Angular 2 development and testing. 66 | See TESTING.rst. 67 | [emanlove] 68 | 69 | - Temporarily removed diagnostic call for retrieving pending HTTP 70 | requests when `Wait For Angular` keyword fails. 71 | [emanlove] 72 | 73 | 0.0.4 (2016-09-12) 74 | ------------------ 75 | Changes: 76 | 77 | - Added implicit Wait on Angular when finding elements by locator. 78 | [emanlove] 79 | 80 | - Added more documentation on testing the library and how the AngularJS 81 | Library is implementing the implicit wait for angular functionality. 82 | See TESTING.rst. 83 | [emanlove] 84 | 85 | 0.0.3 (2016-07-30) 86 | ------------------ 87 | Fixes: 88 | 89 | - Fixed issue with binding locators when no stratergy specified, e.g. 90 | Click Element {{example.binding}} 91 | 92 | Changes: 93 | 94 | - Removed interpolation brackets on Find By Binding criteria for 95 | AngularJS 1.3+ compatability. 96 | [emanlove] 97 | 98 | - Changed the implementation of the `Wait For Angular` keyword. 99 | [emanlove] 100 | 101 | - Added debug statements when `Wait For Angular` times out. Shows 102 | pending https requests only. The list of pending timeouts comes 103 | from functionality that Protractor adds. This has not yet be implemented 104 | in the AngularJSLibrary. 105 | [emanlove] 106 | 107 | - Added documentation on testing the library. See TESTING.rst. 108 | [emanlove] 109 | 110 | - Add library test cases. 111 | [Protractor Team][emanlove] 112 | 113 | 0.0.2 (2016-02-16) 114 | ------------------ 115 | 116 | Fixes: 117 | 118 | - Updated documentation. 119 | [tisto][emanlove] 120 | 121 | - Resolved issue when using implicit xpath and AngularJS Library. 122 | [emanlove] 123 | 124 | 0.0.1 (2016-02-06) 125 | ------------------ 126 | 127 | New: 128 | 129 | - Initial Release 130 | [zephraph][emanlove] 131 | -------------------------------------------------------------------------------- /DEVELOPERS.rst: -------------------------------------------------------------------------------- 1 | Release procedures 2 | ------------------ 3 | These are the steps to build and push out a release of the AngularJS Library. 4 | 5 | .. code:: bash 6 | 7 | virtualenv -p /usr/bin/python2.7 --no-site-packages release-python27-env 8 | 9 | source release-python27-env/bin/activate 10 | 11 | pip install -U pip 12 | pip install twine wheel 13 | 14 | python setup.py sdist bdist_egg bdist_wheel 15 | 16 | twine upload -r pypi dist/* 17 | 18 | Alternatively one can specify the username to use on the public repository, in 19 | this case PyPI, using 20 | 21 | .. code:: bash 22 | 23 | twine upload -r pypi -u dist/* 24 | 25 | Finally to tag the repository use 26 | 27 | .. code:: bash 28 | 29 | git tag -a v0.0.5 -m "0.0.5 release" 30 | git push --tags 31 | 32 | Note if one forgets to tag a release and needs to do so after later commits have 33 | been made, one can use 34 | 35 | .. code:: bash 36 | 37 | git tag -a v0.0.5 -m "0.0.5 release" 38 | 39 | to tag a specified commit. 40 | 41 | Steps to update keyword documentation 42 | ------------------------------------- 43 | 44 | .. code:: bash 45 | 46 | git checkout v0.0.5 47 | python -m robot.libdoc AngularJSLibrary docs/test.html 48 | 49 | Current Steps to Setup Development Environment and Run Tests 50 | ------------------------------------------------------------ 51 | Here are the current (as of July 25, 2022, selenium==4.3.0, robotframework-seleniumlibrary==6.1.0.dev1, protractor==6.0.0) instructions for setting up the development environment and running the tests 52 | 53 | .. code:: bash 54 | 55 | mkdir ng-test 56 | cd ng-test/ 57 | git clone https://github.com/robotframework/SeleniumLibrary.git rf-sl 58 | git clone https://github.com/MarketSquare/robotframework-angularjs.git rf-ng 59 | git clone https://github.com/angular/protractor.git ptor 60 | 61 | virtualenv -p /usr/bin/python3.9 cl-py39-env 62 | source cl-py39-env/bin/activate 63 | pip install robotframework robotstatuschecker mockito selenium requests pytest 64 | 65 | patch rf-sl/atest/resources/testserver/testserver.py rf-ng/AngularJSLibrary/testserver.py.patch 66 | 67 | cp -R ptor/testapp rf-sl/atest/resources/. 68 | 69 | cp rf-ng/AngularJSLibrary/async.html rf-sl/atest/resources/testapp/ng1/async/. 70 | cp rf-ng/AngularJSLibrary/async.js rf-sl/atest/resources/testapp/ng1/async/. 71 | 72 | cp rf-ng/AngularJSLibrary/angular.robot rf-sl/atest/acceptance/locators/. 73 | cp rf-ng/AngularJSLibrary/angular_wait.robot rf-sl/atest/acceptance/keywords/. 74 | 75 | cd rf-sl 76 | python atest/run.py FF --suite angular --pythonpath ../rf-ng 77 | python atest/run.py FF --suite angular_wait --pythonpath ../rf-ng 78 | 79 | or if you are using Windows 80 | 81 | .. code:: bat 82 | 83 | mkdir test-ng 84 | cd test-ng 85 | 86 | git clone https://github.com/robotframework/SeleniumLibrary.git rf-sl 87 | git clone https://github.com/Selenium2Library/robotframework-angularjs.git rf-ng 88 | git clone https://github.com/angular/protractor.git ptor 89 | 90 | virtualenv -p C:\Python27\python.exe --no-site-packages cl-py27-env 91 | cl-py27-env\Scripts\activate 92 | 93 | pip install robotframework robotstatuschecker mockito selenium 94 | 95 | REM There is no default patch command under MS Dos so this step needs 96 | REM to be manually implemented. 97 | REM patch rf-sl/atest/resources/testserver/testserver.py rf-ng/AngularJSLibrary/testserver.py.patch 98 | 99 | xcopy ptor\testapp rf-sl\atest\resources\testapp\ /E /Y /F 100 | copy /Y rf-ng\AngularJSLibrary\async.html rf-sl\atest\resources\testapp\ng1\async\. 101 | copy /Y rf-ng\AngularJSLibrary\async.js rf-sl\atest\resources\testapp\ng1\async\. 102 | copy rf-ng\AngularJSLibrary\angular.robot rf-sl\atest\acceptance\locators\. 103 | copy rf-ng\AngularJSLibrary\angular_wait.robot rf-sl\atest\acceptance\keywords\. 104 | 105 | and then to run the tests 106 | 107 | .. code:: bat 108 | 109 | cd rf-sl 110 | python atest\run.py FF --nounit --suite angular --pythonpath ..\rf-ng 111 | python atest\run.py FF --nounit --suite angular_wait --pythonpath ..\rf-ng 112 | 113 | noting in the commands above the addition of :code:`--nounit` argument to forgo running the unit tests. 114 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Apache License 5 | Version 2.0, January 2004 6 | http://www.apache.org/licenses/ 7 | 8 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 9 | 10 | 1. Definitions. 11 | 12 | "License" shall mean the terms and conditions for use, reproduction, 13 | and distribution as defined by Sections 1 through 9 of this document. 14 | 15 | "Licensor" shall mean the copyright owner or entity authorized by 16 | the copyright owner that is granting the License. 17 | 18 | "Legal Entity" shall mean the union of the acting entity and all 19 | other entities that control, are controlled by, or are under common 20 | control with that entity. For the purposes of this definition, 21 | "control" means (i) the power, direct or indirect, to cause the 22 | direction or management of such entity, whether by contract or 23 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 24 | outstanding shares, or (iii) beneficial ownership of such entity. 25 | 26 | "You" (or "Your") shall mean an individual or Legal Entity 27 | exercising permissions granted by this License. 28 | 29 | "Source" form shall mean the preferred form for making modifications, 30 | including but not limited to software source code, documentation 31 | source, and configuration files. 32 | 33 | "Object" form shall mean any form resulting from mechanical 34 | transformation or translation of a Source form, including but 35 | not limited to compiled object code, generated documentation, 36 | and conversions to other media types. 37 | 38 | "Work" shall mean the work of authorship, whether in Source or 39 | Object form, made available under the License, as indicated by a 40 | copyright notice that is included in or attached to the work 41 | (an example is provided in the Appendix below). 42 | 43 | "Derivative Works" shall mean any work, whether in Source or Object 44 | form, that is based on (or derived from) the Work and for which the 45 | editorial revisions, annotations, elaborations, or other modifications 46 | represent, as a whole, an original work of authorship. For the purposes 47 | of this License, Derivative Works shall not include works that remain 48 | separable from, or merely link (or bind by name) to the interfaces of, 49 | the Work and Derivative Works thereof. 50 | 51 | "Contribution" shall mean any work of authorship, including 52 | the original version of the Work and any modifications or additions 53 | to that Work or Derivative Works thereof, that is intentionally 54 | submitted to Licensor for inclusion in the Work by the copyright owner 55 | or by an individual or Legal Entity authorized to submit on behalf of 56 | the copyright owner. For the purposes of this definition, "submitted" 57 | means any form of electronic, verbal, or written communication sent 58 | to the Licensor or its representatives, including but not limited to 59 | communication on electronic mailing lists, source code control systems, 60 | and issue tracking systems that are managed by, or on behalf of, the 61 | Licensor for the purpose of discussing and improving the Work, but 62 | excluding communication that is conspicuously marked or otherwise 63 | designated in writing by the copyright owner as "Not a Contribution." 64 | 65 | "Contributor" shall mean Licensor and any individual or Legal Entity 66 | on behalf of whom a Contribution has been received by Licensor and 67 | subsequently incorporated within the Work. 68 | 69 | 2. Grant of Copyright License. Subject to the terms and conditions of 70 | this License, each Contributor hereby grants to You a perpetual, 71 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 72 | copyright license to reproduce, prepare Derivative Works of, 73 | publicly display, publicly perform, sublicense, and distribute the 74 | Work and such Derivative Works in Source or Object form. 75 | 76 | 3. Grant of Patent License. Subject to the terms and conditions of 77 | this License, each Contributor hereby grants to You a perpetual, 78 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 79 | (except as stated in this section) patent license to make, have made, 80 | use, offer to sell, sell, import, and otherwise transfer the Work, 81 | where such license applies only to those patent claims licensable 82 | by such Contributor that are necessarily infringed by their 83 | Contribution(s) alone or by combination of their Contribution(s) 84 | with the Work to which such Contribution(s) was submitted. If You 85 | institute patent litigation against any entity (including a 86 | cross-claim or counterclaim in a lawsuit) alleging that the Work 87 | or a Contribution incorporated within the Work constitutes direct 88 | or contributory patent infringement, then any patent licenses 89 | granted to You under this License for that Work shall terminate 90 | as of the date such litigation is filed. 91 | 92 | 4. Redistribution. You may reproduce and distribute copies of the 93 | Work or Derivative Works thereof in any medium, with or without 94 | modifications, and in Source or Object form, provided that You 95 | meet the following conditions: 96 | 97 | (a) You must give any other recipients of the Work or 98 | Derivative Works a copy of this License; and 99 | 100 | (b) You must cause any modified files to carry prominent notices 101 | stating that You changed the files; and 102 | 103 | (c) You must retain, in the Source form of any Derivative Works 104 | that You distribute, all copyright, patent, trademark, and 105 | attribution notices from the Source form of the Work, 106 | excluding those notices that do not pertain to any part of 107 | the Derivative Works; and 108 | 109 | (d) If the Work includes a "NOTICE" text file as part of its 110 | distribution, then any Derivative Works that You distribute must 111 | include a readable copy of the attribution notices contained 112 | within such NOTICE file, excluding those notices that do not 113 | pertain to any part of the Derivative Works, in at least one 114 | of the following places: within a NOTICE text file distributed 115 | as part of the Derivative Works; within the Source form or 116 | documentation, if provided along with the Derivative Works; or, 117 | within a display generated by the Derivative Works, if and 118 | wherever such third-party notices normally appear. The contents 119 | of the NOTICE file are for informational purposes only and 120 | do not modify the License. You may add Your own attribution 121 | notices within Derivative Works that You distribute, alongside 122 | or as an addendum to the NOTICE text from the Work, provided 123 | that such additional attribution notices cannot be construed 124 | as modifying the License. 125 | 126 | You may add Your own copyright statement to Your modifications and 127 | may provide additional or different license terms and conditions 128 | for use, reproduction, or distribution of Your modifications, or 129 | for any such Derivative Works as a whole, provided Your use, 130 | reproduction, and distribution of the Work otherwise complies with 131 | the conditions stated in this License. 132 | 133 | 5. Submission of Contributions. Unless You explicitly state otherwise, 134 | any Contribution intentionally submitted for inclusion in the Work 135 | by You to the Licensor shall be under the terms and conditions of 136 | this License, without any additional terms or conditions. 137 | Notwithstanding the above, nothing herein shall supersede or modify 138 | the terms of any separate license agreement you may have executed 139 | with Licensor regarding such Contributions. 140 | 141 | 6. Trademarks. This License does not grant permission to use the trade 142 | names, trademarks, service marks, or product names of the Licensor, 143 | except as required for reasonable and customary use in describing the 144 | origin of the Work and reproducing the content of the NOTICE file. 145 | 146 | 7. Disclaimer of Warranty. Unless required by applicable law or 147 | agreed to in writing, Licensor provides the Work (and each 148 | Contributor provides its Contributions) on an "AS IS" BASIS, 149 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 150 | implied, including, without limitation, any warranties or conditions 151 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 152 | PARTICULAR PURPOSE. You are solely responsible for determining the 153 | appropriateness of using or redistributing the Work and assume any 154 | risks associated with Your exercise of permissions under this License. 155 | 156 | 8. Limitation of Liability. In no event and under no legal theory, 157 | whether in tort (including negligence), contract, or otherwise, 158 | unless required by applicable law (such as deliberate and grossly 159 | negligent acts) or agreed to in writing, shall any Contributor be 160 | liable to You for damages, including any direct, indirect, special, 161 | incidental, or consequential damages of any character arising as a 162 | result of this License or out of the use or inability to use the 163 | Work (including but not limited to damages for loss of goodwill, 164 | work stoppage, computer failure or malfunction, or any and all 165 | other commercial damages or losses), even if such Contributor 166 | has been advised of the possibility of such damages. 167 | 168 | 9. Accepting Warranty or Additional Liability. While redistributing 169 | the Work or Derivative Works thereof, You may choose to offer, 170 | and charge a fee for, acceptance of support, warranty, indemnity, 171 | or other liability obligations and/or rights consistent with this 172 | License. However, in accepting such obligations, You may act only 173 | on Your own behalf and on Your sole responsibility, not on behalf 174 | of any other Contributor, and only if You agree to indemnify, 175 | defend, and hold each Contributor harmless for any liability 176 | incurred by, or claims asserted against, such Contributor by reason 177 | of your accepting any such warranty or additional liability. 178 | 179 | END OF TERMS AND CONDITIONS 180 | 181 | APPENDIX: How to apply the Apache License to your work. 182 | 183 | To apply the Apache License to your work, attach the following 184 | boilerplate notice, with the fields enclosed by brackets "[]" 185 | replaced with your own identifying information. (Don't include 186 | the brackets!) The text should be enclosed in the appropriate 187 | comment syntax for the file format. We also recommend that a 188 | file or class name and description of purpose be included on the 189 | same "printed page" as the copyright notice for easier 190 | identification within third-party archives. 191 | 192 | Copyright 2015 Selenium2Library 193 | 194 | Licensed under the Apache License, Version 2.0 (the "License"); 195 | you may not use this file except in compliance with the License. 196 | You may obtain a copy of the License at 197 | 198 | http://www.apache.org/licenses/LICENSE-2.0 199 | 200 | Unless required by applicable law or agreed to in writing, software 201 | distributed under the License is distributed on an "AS IS" BASIS, 202 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 203 | See the License for the specific language governing permissions and 204 | limitations under the License. 205 | 206 | AngularJSLibrary\ng-repeater.js: 207 | AngularJSLibrary\ng-repeater.min.js: 208 | AngularJSLibrary\__init__.py (certain javascript portions): 209 | Various code examples (as noted in the documentation): 210 | 211 | The MIT License 212 | 213 | Copyright (c) 2010-2020 Google LLC. http://angularjs.org 214 | Copyright (c) 2010-2022 Google LLC. http://angular.io/license 215 | 216 | Permission is hereby granted, free of charge, to any person obtaining a copy 217 | of this software and associated documentation files (the "Software"), to deal 218 | in the Software without restriction, including without limitation the rights 219 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 220 | copies of the Software, and to permit persons to whom the Software is 221 | furnished to do so, subject to the following conditions: 222 | 223 | The above copyright notice and this permission notice shall be included in 224 | all copies or substantial portions of the Software. 225 | 226 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 227 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 228 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 229 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 230 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 231 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 232 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | AngularJSLibrary - robotframework-angularjs 2 | =========================================== 3 | An AngularJS and Angular extension to Robotframework's SeleniumLibrary. 4 | AngularJSLibrary primarily provides functionality to deal with **waiting** and 5 | thus timing issue when testing Angular based websites. The library does this by 6 | providing first an implicit wait and, subsequently, an explicit keyword for 7 | waiting on angular. 8 | 9 | About this library 10 | ------------------ 11 | The AngularJSLibrary, despite the name including JS, supports testing against 12 | both Angular 2.0+ (known as simply Angular) and Angular 1.0 (also known as 13 | Angular JS). 14 | 15 | This library is considered mature and feature complete. Ongoing support is 16 | provided through the Robot Framework community Slack. Thus it may appear 17 | to be abandoned or neglected for which it is not. 18 | 19 | **Please carefully read through this README in its entirety**. It covers how 20 | to configure and import the library into your test scripts, use and understand 21 | its key functionality, as well as troubleshooting and debugging information. 22 | 23 | Installation 24 | ------------ 25 | To install **AngularJSLibrary**, run: 26 | 27 | .. code:: bash 28 | 29 | pip install robotframework-angularjs 30 | 31 | 32 | Alternatively, to install from source: 33 | 34 | .. code:: bash 35 | 36 | python setup.py install 37 | 38 | 39 | Identifying the Angular root element 40 | ------------------------------------ 41 | Prior to importing the library, one must identify the Angular root element or root 42 | component. For more information about 43 | 44 | Here are a few examples of Angular sites and their corresponding root elements or 45 | components. The first example is from the `AngularJS.org PhoneCat tutorial `_. 46 | The base html code is 47 | 48 | .. code:: html 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 |
    57 |
    58 |
    59 | 60 | 61 | 62 | 63 | In the PhoneCat tutorial the html element with the ng-app attribute is the root 64 | element. Thus for this website the root selector would be :code:`[ng-app]`. The 65 | next example is the `Getting started with Angular tutorial `_ 66 | on angular.io site. It's main html looks like 67 | 68 | .. code:: html 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | Here the root component is the app-root element and thus the root selector for 81 | this website would be :code:`app-root`. The last example is the `example tab of 82 | the Dialog UI component `_ 83 | within the Angular.io Component Dev Kit (CDK). 84 | 85 | .. code:: html 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | The root component for the Dialog component example page is the material-docs-app 99 | element. The root selector will be :code:`material-docs-app`. 100 | 101 | Now we will use the root selector when we import the library. 102 | 103 | Importing the library 104 | --------------------- 105 | The proper name for importing the library is :code:`AngularJSLibrary`. You will 106 | need to include the SeleniumLibrary **before** you import the AngularJSLibrary. 107 | The first of two library options is `root_selector`. So using our first example, 108 | the PhoneCat tutorial from AngularJS.org above, our import may look like, 109 | 110 | .. code:: robotframework 111 | 112 | *** Settings *** 113 | Library SeleniumLibrary 114 | Library AngularJSLibrary root_selector=[ng-app] 115 | 116 | *** Test Cases *** 117 | Search Through The Phone Catalog For Samsung Phones 118 | Open Browser http://angular.github.io/angular-phonecat/step-14/app Chrome 119 | Input Text //input Samsung 120 | Click Link Samsung Galaxy Tab™ 121 | Element Text Should Be css:phone-detail h1 Samsung Galaxy Tab™ 122 | 123 | As the default value for the root_selector argument is :code:`[ng-app]`, for 124 | the PhoneCat tutorial we did not need to specify the root_selector and could 125 | have written the Library import as 126 | 127 | .. code:: robotframework 128 | 129 | *** Settings *** 130 | Library SeleniumLibrary 131 | Library AngularJSLibrary 132 | 133 | *** Test Cases *** 134 | Search Through The Phone Catalog For Samsung Phones 135 | Open Browser http://angular.github.io/angular-phonecat/step-14/app Chrome 136 | Input Text //input Samsung 137 | Click Link Samsung Galaxy Tab™ 138 | Element Text Should Be css:phone-detail h1 Samsung Galaxy Tab™ 139 | 140 | *If you get an "Unable to find root selector ..." error* then you should re-check 141 | your root_selector. Note that unlike locators used with the SeleniumLibrary the 142 | root_selector **should not** contain the css locator prefix. 143 | 144 | The second library option, ignore_implicit_angular_wait, is a flag which when 145 | set to True the AngularJS Library will not wait for Angular $timeouts nor 146 | $http calls to complete when finding elements by locator. The default value is 147 | False. 148 | 149 | *If the application under test starts on a non angular page,* for example a 150 | login page that is not angular which leads into an angular app, then one should 151 | start with the implicit angular wait turned off. For example, 152 | 153 | .. code:: robotframework 154 | 155 | *** Settings *** 156 | Library SeleniumLibrary 157 | Library AngularJSLibrary ignore_implicit_angular_wait=True 158 | 159 | *** Test Cases *** 160 | Login Into Non Angular Page 161 | # ... 162 | 163 | Usage of the Waiting functionality 164 | ---------------------------------- 165 | The AngularJS Library provides two types of waiting: a built-in implicit wait 166 | that automatically waits when using a locator strategy and then an explicit 167 | keyword that one calls out or writes into their script. In the tutorial and 168 | examples above the scripts there aren't any expicit wait calls. Here instead 169 | the script is relying on the implicit wait which by default is turned on. 170 | This means as soon as you import the library you will have waiting enabled. 171 | 172 | This can be demostrated by importing the library with the implicit wait turned 173 | off and using instead the library's explicit `Wait For Angular` keyword. 174 | 175 | .. code:: robotframework 176 | 177 | *** Settings *** 178 | Library SeleniumLibrary 179 | Library AngularJSLibrary ignore_implicit_angular_wait=True 180 | 181 | *** Test Cases *** 182 | Search Through The Phone Catalog For Samsung Phones 183 | Open Browser http://angular.github.io/angular-phonecat/step-14/app Chrome 184 | Wait For Angular 185 | Input Text //input Samsung 186 | Wait For Angular 187 | Click Link Samsung Galaxy Tab™ 188 | Wait For Angular 189 | Element Text Should Be css:phone-detail h1 Samsung Galaxy Tab™ 190 | 191 | With the implicit wait functionality it is expected that most of the situations 192 | where waiting is needed will be handled "automatically" by this "hidden" implicit 193 | wait. Thus if one examined your test case they would not see many, if any, 194 | `Wait For Angular` keywords but instead would see actions keywords with no 195 | "waiting" keywords in between actions. There are times, though, when one needs to 196 | explicitly call out to wait for angular. For example when using a SeleniumLibrary 197 | keyword that does not use a locator strategy, like :code:`Alert Should Be Present` 198 | and :code:`Page should contain`, or if you use webelement. 199 | 200 | In addition to the option to turn off the implicit wait on library import, you 201 | may turn it off using the :code:`Set Ignore Implicit Angular Wait` keyword with 202 | an argument of :code:`${True}`. 203 | 204 | 205 | Understanding and verifying the angular waits 206 | --------------------------------------------- 207 | Although the waits seem like "Magic" they are not. Let's look into how the 208 | waits are implimented and work to gain insight as to how they work. The waits, 209 | both the implicit and explicit, poll what I call the "angular queue". 210 | Technically it is checking that angular has "finished rendering and has no 211 | outstanding $http or $timeout calls". It does this by checking the 212 | `notifyWhenNoOutstandingRequests` function for AngularJS applications. For 213 | Angular applications the library is checking the `isStable` function on the 214 | Angular Testibility service. 215 | 216 | This can be seen within the log file by setting the loglevel to DEBUG or TRACE. 217 | Rerunning the PhoneCat demo (:code:`robot --loglevel DEBUG demo_phonecat.robot`) 218 | one should see in the log file 219 | 220 | .. code:: robotframework 221 | 222 | 20:01:04.658 INFO Typing text 'Samsung' into text field '//input'. 223 | 20:01:04.658 DEBUG POST http://localhost:50271/session/f75e7aaf5a00c717ae5e4af34a6ce516540611dae4b7f6079ce1a753c308cde2/execute/sync {"script": "...snip..."]} 224 | 20:01:04.661 DEBUG http://localhost:50271 "POST /session/f75e7aaf5a00c717ae5e4af34a6ce516540611dae4b7f6079ce1a753c308cde2/execute/sync HTTP/1.1" 200 14 225 | 20:01:04.661 DEBUG Remote response: status=200 | data={"value":true} | headers=HTTPHeaderDict({'Content-Length': '14', 'Content-Type': 'application/json; charset=utf-8', 'cache-control': 'no-cache'}) 226 | 20:01:04.661 DEBUG Finished Request 227 | 228 | For space reasons I snipped out the core script on the POST execute/sync line. 229 | One should see these lines repeated several times over. This is the polling the 230 | library is doing to see if the application is ready to test. It will repeat 231 | this query till either it returns true or it will repeat till the "give up" 232 | timeout. If it gives up, it will silently and gracefully fail continuing onto 233 | the actions it was waiting to perform. It is important for the user of this 234 | library to see and understand, at a basic level, this functionality. As the 235 | primary usage are these implicit, and thus hidden, waits it is key to see how 236 | to check the library is operating properly and when it is waiting. 237 | 238 | *When using the AngularJS Library, if all waits timeout then the AngularJS 239 | Library may not wait properly with that application under test.* This, 240 | recalling all previously outlined information, is telling you that the 241 | Angular app is constantly busy. This can happen depending on how the angular 242 | application is designed. It may also affect only a portion of the application 243 | so it is important to test out various parts of the application. 244 | 245 | Further debugging techniques 246 | ---------------------------- 247 | In addition to using the AngularJS Library, one can use the Browser's DevTools 248 | as a way to test out and demonstrate the core operation of the library against 249 | an application. To be clear, this is not library code but similar Javascript 250 | code which one uses outside of robot to exhibit, to a dev team for example, 251 | what the library is seeing when it querys the application. When viewing the 252 | application under test open the DevTools, preferably under Chrome, and on the 253 | Console tab type the following, 254 | 255 | If the application is built with AngularJS or Angular 1.x then the script is 256 | 257 | .. code:: javascript 258 | 259 | var callback = function () {console.log('*')} 260 | var el = document.querySelector('[ng-app]'); 261 | var h = setInterval(function w4ng() { 262 | console.log('.'); 263 | try { 264 | angular.element(el).injector().get('$browser'). 265 | notifyWhenNoOutstandingRequests(callback); 266 | } catch (err) { 267 | console.log(err.message); 268 | callback(err.message); 269 | } 270 | }, 10); 271 | 272 | For Angular v2+ then the script is 273 | 274 | .. code:: javascript 275 | 276 | var callback = function () {console.log('*')} 277 | var el = document.querySelector('material-docs-app'); 278 | var h = setInterval(function w4ng() { 279 | console.log('.'); 280 | try { 281 | var readyToTest = window.getAngularTestability(el).isStable(); 282 | } catch (err) { 283 | console.log(err.message); 284 | callback(err.message); 285 | } 286 | if (!readyToTest) { 287 | callback() 288 | } else { 289 | console.log('.'); 290 | } 291 | }, 10); 292 | 293 | This will display a :code:`.` when "stable". Otherwise it will show a :code:`*` 294 | when "busy". To shut down the javascript interval and stop this script type on 295 | the console prompt :code:`clearInterval(h);`. [Chrome Browser is preferred 296 | because repeated output within its DevTools console will be displayed as a 297 | single line with a count versus a new line for each output making it much 298 | easier to see and read.] I have personally used this myself both in developing 299 | this library as well as demonstrating to various Angular developers how a 300 | design/implementation is blocking testability. 301 | 302 | Additional Angular Specific Locator Strategies 303 | ------------------------------------------------- 304 | **Note: It is no longer recommended to use these angular specific locator 305 | strategies. Although functional, the SeleniumLibrary locator strategies are more 306 | than sufficient and in most cases easier to use then these strategies. For backward 307 | compatablity reasons these will be left in but it is strongly recommended not to 308 | use.** 309 | 310 | The library provides three new locator strategies, including ``binding``, 311 | ``model``, and ``repeater``. 312 | 313 | For example, you can look for an Angular ng-binding using 314 | 315 | .. code:: robotframework 316 | 317 | Get Text binding={{greeting}} 318 | 319 | 320 | or by using partial match 321 | 322 | .. code:: robotframework 323 | 324 | Get Text binding=greet 325 | 326 | 327 | or by simply using the binding {{…}} notation 328 | 329 | .. code:: robotframework 330 | 331 | Get Text {{greeting}} 332 | 333 | 334 | One can also find elements by model 335 | 336 | .. code:: robotframework 337 | 338 | Input Text model=aboutbox Something else to write about 339 | 340 | 341 | .. role:: rf(code) 342 | :language: robotframework 343 | 344 | Finally there is the strategy of find by repeat. This takes the general form of :rf:`repeater=some ngRepeat directive@row[n]@column={{ngBinding}}`. Here we specify the directive as well as the row, an zero-based index, and the column, an ngBinding. Using this full format will return, if exists the element matching the directive, row and column binding. One does not need to specify the row and column but can specify either both, one or the other or neither. In such cases the locator may return list of elements or even a list of list of elements. Also the ordering of row and column does not matter; using :rf:`repeater=baz in days@row[0]@column=b` is the same as :rf:`repeater=baz in days@column=b @row[0]`. 345 | 346 | 347 | Getting Help 348 | ------------ 349 | If you need help with AngularJSLibrary, SeleniumLibrary, or Robot Framework usage, please reach out within the Robot Framework community `Slack `_. 350 | 351 | 352 | Keyword Documentation 353 | --------------------- 354 | The keyword documentation can be found on the `Github project page `_. 355 | 356 | 357 | Testing 358 | ------- 359 | For information on how we test the AngularJSLibrary see the `Testing.rst `_ file. 360 | 361 | 362 | References 363 | ---------- 364 | 365 | `SeleniumLibrary `_: Web testing library for Robot Framework 366 | 367 | `Protractor `_: E2E test framework for Angular apps 368 | 369 | `isStable reference `_ 370 | 371 | `Angular Testability service `_ 372 | -------------------------------------------------------------------------------- /TESTING.rst: -------------------------------------------------------------------------------- 1 | Testing AngularJS Library 2 | ========================= 3 | 4 | These are instructions for pulling in all the parts and testing the AngularJS Library for Robot Framework 5 | 6 | Setup Environment 7 | ----------------- 8 | 9 | We will have both a base set of pythons packages as well as the source for the AngularJSLibrary and the Selenium2Library all of which will will want to keep isolated from your system python and its packages. As such we will use Python's virtual environment. Let's start by creating a a root folder for testing. 10 | 11 | .. code:: bash 12 | 13 | mkdir test-ng 14 | cd test-ng 15 | 16 | Within this root folder we will create the virtualenv and clone source repositories 17 | 18 | .. code:: bash 19 | 20 | virtualenv -p /usr/bin/python2.7 --no-site-packages clean-python27-env 21 | source clean-python27-env/bin/activate 22 | pip install decorator docutils robotframework selenium 23 | 24 | git clone git@github.com:Selenium2Library/robotframework-angularjs.git rf-ng 25 | git clone git@github.com:robotframework/Selenium2Library.git rf-s2l 26 | 27 | We will also clone the protractor repository. From Protractor we will use their test site, testapp, but not their test server. For the test server we will use the Selenium2Library test server with some modifications. 28 | 29 | .. code:: bash 30 | 31 | git clone git@github.com:angular/protractor.git ptor 32 | cp -R ptor/testapp rf-s2l/test/resources/. 33 | 34 | I modified the async testapp page so that the implicit wait for angular functionality can be tested. The modified version of async.html and async.js can be moved over to the testapp directory under rf-s2l directory. 35 | 36 | .. code:: bash 37 | 38 | cp rf-ng/AngularJSLibrary/async.html rf-s2l/test/resources/testapp/ng1/async/. 39 | cp rf-ng/AngularJSLibrary/async.js rf-s2l/test/resources/testapp/ng1/async/. 40 | 41 | Modifying the test server of Selenium2Library, rf-s2l\\test\\resources\\testserver\\testserver.py, add the following method, do_GET, to the StoppableHttpRequestHandler class. 42 | 43 | .. code:: python 44 | 45 | def do_GET(self): 46 | """Response pages for Angular tests. 47 | 48 | Added by Edward Manlove - June 5, 2014 49 | """ 50 | if self.path.endswith('/fastcall'): 51 | self.send_response(200) 52 | self.send_header('Content-type', 'text/html') 53 | self.end_headers() 54 | self.wfile.write('done') 55 | elif self.path.endswith('/slowcall'): 56 | sleep(2) 57 | self.send_response(200) 58 | self.send_header('Content-type', 'text/html') 59 | self.end_headers() 60 | self.wfile.write('finally done') 61 | elif self.path.endswith('/fastTemplateUrl'): 62 | self.send_response(200) 63 | self.send_header('Content-type', 'text/html') 64 | self.end_headers() 65 | self.wfile.write('fast template contents') 66 | elif self.path.endswith('/slowTemplateUrl'): 67 | sleep(2) 68 | self.send_response(200) 69 | self.send_header('Content-type', 'text/html') 70 | self.end_headers() 71 | self.wfile.write('slow template contents') 72 | else: 73 | SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET(self) 74 | 75 | Don't forget with the added sleep statements you need to include the time package 76 | 77 | .. code:: python 78 | 79 | from time import sleep 80 | 81 | otherwise several tests will fail. 82 | 83 | Finally, let's move the test files over to the Selenium2Library test directory. Although this may not be necessary I do it to keep all the test files together. Ultimately I would like to see the Selenium2Library test directory moved into the src directory so the tests get distributed and then allow the test scripts for AngularJSLibrary be abe to be run from its own test directory. But for now we will combine them. 84 | 85 | .. code:: bash 86 | 87 | cp rf-ng/AngularJSLibrary/angular.robot rf-s2l/test/acceptance/locators/. 88 | cp rf-ng/AngularJSLibrary/angular_wait.robot rf-s2l/test/acceptance/keywords/. 89 | 90 | Directory Structure 91 | ------------------- 92 | 93 | So taking a step back and looking at the whole structure we should see the following directories 94 | 95 | rf-s2l/ 96 | The source code for Robot Framework Selenium2Library. 97 | 98 | rf-ng/ 99 | The source code for Robot Framework AngularJSLibrary. 100 | 101 | ptor/ 102 | The source code for Robot Framework Seleniu2Library. 103 | 104 | Within those directories we should see some modifications 105 | 106 | rf-s2l/test/resources/testserver/testserver.py 107 | A modified version of the test server containing the additional do_GET() method. 108 | 109 | rf-s2l/test/acceptance/locators/angular.robot 110 | AngularJSLibrary acceptance tests testing locators. 111 | 112 | rf-s2l/test/acceptance/keywords/angular_wait.robot 113 | AngularJSLibrary acceptance tests testing wait for angular functionality. 114 | 115 | rf-s2l/test/resources/testapp/ng/async/async.html 116 | rf-s2l/test/resources/testapp/ng/async/async.js 117 | A modified version of the async testapp page containing buttons which appear after the 118 | angular $timeouts and $http requests are completed. 119 | 120 | And if we activate our virtual Python instance we should see 121 | 122 | .. code:: bash 123 | 124 | # pip list 125 | decorator (4.0.10) 126 | docutils (0.12) 127 | pip (8.1.2) 128 | robotframework (3.0) 129 | selenium (2.53.6) 130 | setuptools (8.2.1) 131 | 132 | Note your versions may be different then mine listed here but key is you have installed robotframework and selenium packages and have **not** installed selenium2library as we will use the source code instead. 133 | 134 | Starting the modified testserver 135 | -------------------------------- 136 | 137 | Open a new bash terminal from which we will run the test sever 138 | 139 | .. code:: bash 140 | 141 | cd test-ng 142 | 143 | source clean-python27-env/bin/activate 144 | 145 | cd rf-s2l 146 | 147 | python test/resources/testserver/testserver.py start 148 | 149 | You can test the server by navigating in a browser to 150 | 151 | .. code:: 152 | 153 | http://localhost:7000/testapp 154 | 155 | Running the test scripts 156 | ------------------------ 157 | 158 | In another terminal we will run the test scripts 159 | 160 | .. code:: bash 161 | 162 | cd ng 163 | 164 | source clean-python27-env/bin/activate 165 | 166 | cd rf-s2l 167 | 168 | python test/run_tests.py python FF --suite acceptance.locators.angular --pythonpath ../rf-ng 169 | 170 | python test/run_tests.py python FF --suite acceptance.keywords.angular_wait --pythonpath ../rf-ng 171 | 172 | Note there is currently an issue with the Selenium2Library test runner script where if you specify a specific suite the output log and report files will not be created automatically. To get those files you can type 173 | 174 | .. code:: bash 175 | 176 | rebot -d test/results/ test/results/output.xml 177 | 178 | Understanding how AngularJSLibrary works 179 | ---------------------------------------- 180 | 181 | It is important for you, the end user, to understand what is going on in the underlying library and there are many reasons for that. For one as I continue to develop this library I realize some initial assumptions and thus original implementations were simply wrong. I also have very narrow focus as my daily work focuses on a single (and usually older) version of AngularJS. So there could be issues I am not seeing and thus not addressing. These and many more reasons support the argument that as a library user we should all be well informed as to how the library works and what is Protractor / AngularJS doing in the functions we are mimicing. 182 | 183 | Let's start off by examining the waitForAngular functionality in Protractor. At the core is this function (with some code removed) in ptor/lib/clientsidescripts.js 184 | 185 | .. code :: javascript 186 | 187 | /** 188 | * Wait until Angular has finished rendering and has 189 | * no outstanding $http calls before continuing. The specific Angular app 190 | * is determined by the rootSelector. 191 | * 192 | * Asynchronous. 193 | * 194 | * @param {string} rootSelector The selector housing an ng-app 195 | * @param {function(string)} callback callback. If a failure occurs, it will 196 | * be passed as a parameter. 197 | */ 198 | functions.waitForAngular = function(rootSelector, callback) { 199 | var el = document.querySelector(rootSelector); 200 | 201 | try { 202 | /* [SNIP] Newer vesions (which ones? not sure) there is a function for waiting. This 203 | one is off the window object. For now we will ignore this method and look at the original 204 | method for waiting... 205 | */ 206 | /* [SNIP] Check to make sure we're on an angular page. */ 207 | if (angular.getTestability) { 208 | /* [SNIP] Another function for waiting that comes from angular's testability api. */ 209 | } else { 210 | /* Another check to verify we are within the ng-app. */ 211 | 212 | angular.element(el).injector().get('$browser'). 213 | notifyWhenNoOutstandingRequests(callback); 214 | 215 | } 216 | } catch (err) { 217 | callback(err.message); 218 | } 219 | }; 220 | 221 | So striping out a lot of the code (see [SNIP]s above), the core is simply this 222 | 223 | .. code :: javascript 224 | 225 | angular.element(el).injector().get('$browser'). 226 | notifyWhenNoOutstandingRequests(callback); 227 | 228 | a method which sounds like will give notification when there are no more outstanding requests or angular "actions". But what does callback do? What exactly does this method look like and how does one thus use it information? To answer what this looks like in practice we can use the testapp above. Start up the test server 229 | 230 | .. code:: bash 231 | 232 | cd ng 233 | 234 | source clean-python27-env/bin/activate 235 | 236 | cd rf-s2l 237 | 238 | python test/resources/testserver/testserver.py start 239 | 240 | In a browser navigate to 241 | 242 | .. code:: 243 | 244 | http://localhost:7000/testapp/ng1/alt_root_index.html#/async 245 | 246 | [You'll see here I am using the angular1 portion of testapp. Also I am using the alt_root_index so I can hardcode which version of Angular1.x I'll want.] With the site running open the developers tools (F12) and in the console editor paste the following code, but before you run it let's tear it apart. 247 | 248 | .. code :: javascript 249 | 250 | var callback = function () {console.log('*')} 251 | var el = document.querySelector('#nested-ng-app'); 252 | var h = setInterval(function w4ng() { 253 | console.log('.'); 254 | try { 255 | angular.element(el).injector().get('$browser'). 256 | notifyWhenNoOutstandingRequests(callback); 257 | } catch (err) { 258 | console.log(err.message); 259 | callback(err.message); 260 | } 261 | }, 10); 262 | 263 | You should see it is basically a call to setInterval which will continually call the function with a 10 ms delay each time till the interval is cleared. The function it is calling basically outputs a dot, '.', and calls the notifyWhenNoOutstandingRequests function from the waitForAngular passing along the callback. That callback will print out a star, '*', to the console. Want to take a guess as to what will happen when you run this code? 264 | 265 | You will see a continual series of dots then stars printed to the console. Now on the async test page click the button label $timeout. Only dots are printed to the console for some time. Then only stars. What is happening at this time? When only the dots are outputed we are waiting for angular. More so, the callback that would print stars has not returned. And when just the stars are print, its all those callbacks returning while we were waiting for angular to complete. Go ahead and click on some of the other asyncrouous actions on the async page and see what the output is. 266 | 267 | Note when you want to stop the output type the following line into the console to stop the continious interval call. 268 | 269 | .. code :: javascript 270 | 271 | clearInterval(h); 272 | 273 | So we can visualize the waiting for angular within javascript and from within the browser. We want, though, to not be in javascript (otherise we would just use Protrator and WebDriverJS) but in python. So let's do something similar with a simple python unittest. 274 | 275 | .. code :: python 276 | 277 | import unittest 278 | from selenium import webdriver 279 | 280 | js_waiting_var=""" 281 | var waiting = true; 282 | var callback = function () {waiting = false;} 283 | var el = document.querySelector('#nested-ng-app'); 284 | angular.element(el).injector().get('$browser'). 285 | notifyWhenNoOutstandingRequests(callback); 286 | return waiting; 287 | """ 288 | 289 | 290 | class ExecuteWaitForAngularTestCase(unittest.TestCase): 291 | 292 | def setUp(self): 293 | self.driver = webdriver.Firefox() 294 | 295 | def test_exe_javascript(self): 296 | driver = self.driver 297 | driver.get("http://localhost:7000/testapp/ng1/alt_root_index.html#/async") 298 | try: 299 | while (True): 300 | waiting = driver.execute_script(js_waiting_var) 301 | print('%s' % waiting) 302 | except KeyboardInterrupt: 303 | pass 304 | 305 | def tearDown(self): 306 | self.driver.close() 307 | 308 | if __name__ == "__main__": 309 | unittest.main() 310 | 311 | I went through a couple interations before settling on the above. Let me go through the syncronous javascript script. First, I like the simplicity of it. One iteration had a couple of calls to notifyWhenNoOutstandingRequests() with the (incorrect) thinking that I needed to ask twice to force the javascript execution stack to push through, if you will, the callback function. Remember, having the callback function return (with false) is the indication we are not waiting. But it turns out this not necessary as the function notifyWhenNoOutstandingRequests immediately calls the callback function if the outstanding request count is zero and thus sets the waiting flag to false. Summarizing, the javascript code sets the waiting flag to true stating we are waiting, calls notifyWhenNoOutstandingRequests and if not waiting sets the flag to false then returns the flag. So with a syncronous call we get back an immediate answers of the state of angular. 312 | 313 | The use of a syncronous call by the AngularJSLibrary differs from other non-WebDriverJS ports of protractor. Almost all other ports use asyncronous javascript call. For this I don't understand [1]_. I understand why I choose a syncronious call but I don't see why asynchronous. So just as above I broke it down I tried to make an asycronous call to do the same. No luck. Then I did the second option, Google. [Note, this is the correct order. I tried something first and then tried Google. This is the best approach because it helps you to really think about the problem and not be trapped by the first answer that comes up.] So I tired Google and ... no luck. Some good resources but nothing worked as expected. Then I had the ah ha moment (which was really a duh moment) - Selenium test code! 314 | 315 | The javascript tests can be found under py/test/selenium/webdriver/common/executing_async_javascript_tests.py. These async tests make more sense (to me at least) but don't give much depth to asyncronous javascript calls. 316 | 317 | ...[I think I need to finish this thought]... 318 | 319 | Implicit Wait for Angular 320 | ------------------------- 321 | As advertised on Protractor's homepage, Protractor "can automatically execute the next step in your test the moment the webpage finishes pending tasks, so you don’t have to worry about waiting for your test and webpage to sync." This implicit wait for angular functionality is implemented at couple points. First, as found in the ElementArrayFinder, "the first time [Protractor is] looking for an element". Second, as noted in protractor/lib/plugins.ts, "[b]etween every webdriver action, Protractor calls browser.waitForAngular() to make sure that Angular has no outstanding $http or $timeout calls." So whenever Protractor looks for an element [2]_ or whenever it makes a Selenium WebDriverJS library call it waits for angular thus fufilling the claim that you no longer need explicit waits. For the AngularJSLibrary then we will also want to wait when looking for an element or when calling a selenium method. 322 | 323 | Interestingly enough, for the Selenium2Library when one makes a selenium call one is also looking for an element. This leads to a really slick (IMHO) solution for the Angular2Library. `Here it is`_... 324 | 325 | .. code :: python 326 | 327 | class ngElementFinder(ElementFinder): 328 | def __init__(self, ignore_implicit_angular_wait=False): 329 | super(ngElementFinder, self).__init__() 330 | self.ignore_implicit_angular_wait = ignore_implicit_angular_wait 331 | 332 | def find(self, browser, locator, tag=None): 333 | timeout = self._s2l.get_selenium_timeout() 334 | timeout = timestr_to_secs(timeout) 335 | 336 | if not self.ignore_implicit_angular_wait: 337 | try: 338 | WebDriverWait(self._s2l._current_browser(), timeout, 0.2)\ 339 | .until_not(lambda x: self._s2l._current_browser().execute_script(js_waiting_var)) 340 | except TimeoutException: 341 | pass 342 | strategy = ElementFinder.find(self, browser, locator, tag=None) 343 | return strategy 344 | 345 | Essentially we override the find method of Selenium2Library. So whenever you pass a locator to one of the Selenium2Library keywords you are calling, implicitly, wait for angular. One can see this in the Robot Framework log file when you have set loglevel to ``DEBUG``. Here is the log file output when we click an element 346 | 347 | .. code :: 348 | 349 | KEYWORD Selenium2Library . Click Element model=show 350 | Documentation: 351 | 352 | Click element identified by `locator`. 353 | Start / End / Elapsed: 20161112 11:45:37.794 / 20161112 11:45:37.917 / 00:00:00.123 354 | 11:45:37.794 INFO Clicking element 'model=show'. 355 | 11:45:37.795 DEBUG POST http://127.0.0.1:54972/hub/session/2d75d46c-de31-4a23-85d5-665234b73eb9/execute {"sessionId": "2d75d46c-de31-4a23-85d5-665234b73eb9", "args": [], "script": "\n var waiting = true;\n var callback = function () {waiting = false;}\n var el = document.querySelector('[ng-app]');\n if (typeof angular.element(el).injector() == \"undefined\") {\n throw new Error('root element ([ng-app]) has no injector.' +\n ' this may mean it is not inside ng-app.');\n }\n angular.element(el).injector().get('$browser').\n notifyWhenNoOutstandingRequests(callback);\n return waiting;\n"} 356 | 11:45:37.804 DEBUG Finished Request 357 | 11:45:37.805 DEBUG POST http://127.0.0.1:54972/hub/session/2d75d46c-de31-4a23-85d5-665234b73eb9/execute {"sessionId": "2d75d46c-de31-4a23-85d5-665234b73eb9", "args": [], "script": "return document.querySelectorAll('[ng-model=\"show\"]');"} 358 | 11:45:37.813 DEBUG Finished Request 359 | 11:45:37.814 DEBUG POST http://127.0.0.1:54972/hub/session/2d75d46c-de31-4a23-85d5-665234b73eb9/element/{087ef768-948b-4a41-ad41-422b49d3a143}/click {"sessionId": "2d75d46c-de31-4a23-85d5-665234b73eb9", "id": "{087ef768-948b-4a41-ad41-422b49d3a143}"} 360 | 11:45:37.916 DEBUG Finished Request 361 | 362 | The first POST is an execute javascript call where the javascript function is the internal wait for angular script. In this case Angular was not waiting and thus the next POST was a call to the find element; in this case a ng-model and another javascript call. One would see a similar call to the implicit wait for angular even if the locator strategy was an id, css, xpath or any other standard locator strategy. As compared to the above example here is the (truncated) output when there is a stack of unfufilled promises 363 | 364 | .. code :: 365 | 366 | KEYWORD Selenium2Library . Click Button css=[ng-click="slowAngularTimeoutHideButton()"] 367 | Documentation: 368 | 369 | Clicks a button identified by `locator`. 370 | Start / End / Elapsed: 20161112 11:53:41.863 / 20161112 11:53:47.127 / 00:00:05.264 371 | 11:53:41.864 INFO Clicking button 'css=[ng-click="slowAngularTimeoutHideButton()"]'. 372 | 11:53:41.865 DEBUG POST http://127.0.0.1:59197/hub/session/2b715259-07c2-41d4-90a8-0fa97e271447/execute {"sessionId": "2b715259-07c2-41d4-90a8-0fa97e271447", "args": [], "script": "\n var waiting = true;\n var callback = function () {waiting = false;}\n var el = document.querySelector('[ng-app]');\n if (typeof angular.element(el).injector() == \"undefined\") {\n throw new Error('root element ([ng-app]) has no injector.' +\n ' this may mean it is not inside ng-app.');\n }\n angular.element(el).injector().get('$browser').\n notifyWhenNoOutstandingRequests(callback);\n return waiting;\n"} 373 | 11:53:41.879 DEBUG Finished Request 374 | 11:53:42.080 DEBUG POST http://127.0.0.1:59197/hub/session/2b715259-07c2-41d4-90a8-0fa97e271447/execute {"sessionId": "2b715259-07c2-41d4-90a8-0fa97e271447", "args": [], "script": "\n var waiting = true;\n var callback = function () {waiting = false;}\n var el = document.querySelector('[ng-app]');\n if (typeof angular.element(el).injector() == \"undefined\") {\n throw new Error('root element ([ng-app]) has no injector.' +\n ' this may mean it is not inside ng-app.');\n }\n angular.element(el).injector().get('$browser').\n notifyWhenNoOutstandingRequests(callback);\n return waiting;\n"} 375 | 11:53:42.096 DEBUG Finished Request 376 | 377 | ... ... ... 378 | 379 | 11:53:47.037 DEBUG POST http://127.0.0.1:59197/hub/session/2b715259-07c2-41d4-90a8-0fa97e271447/execute {"sessionId": "2b715259-07c2-41d4-90a8-0fa97e271447", "args": [], "script": "\n var waiting = true;\n var callback = function () {waiting = false;}\n var el = document.querySelector('[ng-app]');\n if (typeof angular.element(el).injector() == \"undefined\") {\n throw new Error('root element ([ng-app]) has no injector.' +\n ' this may mean it is not inside ng-app.');\n }\n angular.element(el).injector().get('$browser').\n notifyWhenNoOutstandingRequests(callback);\n return waiting;\n"} 380 | 11:53:47.052 DEBUG Finished Request 381 | 11:53:47.053 DEBUG POST http://127.0.0.1:59197/hub/session/2b715259-07c2-41d4-90a8-0fa97e271447/elements {"using": "css selector", "sessionId": "2b715259-07c2-41d4-90a8-0fa97e271447", "value": "[ng-click=\"slowAngularTimeoutHideButton()\"]"} 382 | 11:53:47.058 DEBUG Finished Request 383 | 11:53:47.059 DEBUG POST http://127.0.0.1:59197/hub/session/2b715259-07c2-41d4-90a8-0fa97e271447/element/{e9c1e40c-74c7-44a8-801e-45151329fadc}/click {"sessionId": "2b715259-07c2-41d4-90a8-0fa97e271447", "id": "{e9c1e40c-74c7-44a8-801e-45151329fadc}"} 384 | 11:53:47.127 DEBUG Finished Request 385 | 386 | Note the time before and after the (...); about five seconds has passed. Here I truncated, so this printout is not so long, all the javascript calls asking angular if it has any outstanding promises. Eventually the promise have been fufilled and the script looks for an element and clicks it. 387 | 388 | This DEBUG output comes from the internal AngularJSLibrary acceptance tests 389 | 390 | .. code :: RobotFramework 391 | 392 | Implicit Wait For Angular On Timeout 393 | Wait For Angular 394 | 395 | Click Button css=[ng-click="slowAngularTimeout()"] 396 | 397 | Click Button css=[ng-click="slowAngularTimeoutHideButton()"] 398 | 399 | Implicit Wait For Angular On Timeout With Promise 400 | Wait For Angular 401 | 402 | Click Button css=[ng-click="slowAngularTimeoutPromise()"] 403 | 404 | Click Button css=[ng-click="slowAngularTimeoutPromiseHideButton()"] 405 | 406 | To the Protractor testapp, I added some buttons 407 | 408 | .. code :: html 409 | 410 |
  • 411 | 412 | 413 | 414 |
  • 415 |
  • 416 | 417 | 418 | 419 |
  • 420 |
  • 421 | 422 | 423 | 424 |
  • 425 | 426 | that will become visible when the "timeouts" are completed. As shown in the test above, the script clicks both buttons in succession without any explicit delay in the script. This provides us a good test suite to validate the implicit wait for angular. I also added a function to re-hide the button so the tests can be reset. One more test allows us the ability to validate this click the two buttons without delay will fail if we ignore the implicit wait for angular 427 | 428 | .. code :: robotframework 429 | 430 | Toggle Implicit Wait For Angular Flag 431 | Element Should Not Be Visible css=[ng-click="slowAngularTimeoutHideButton()"] 432 | 433 | Set Ignore Implicit Angular Wait ${true} 434 | 435 | Click Button css=[ng-click="slowAngularTimeout()"] 436 | 437 | Run Keyword And Expect Error * Click Button css=[ng-click="slowAngularTimeoutHideButton()"] 438 | 439 | Wait For Angular 440 | Element Should Be Visible css=[ng-click="slowAngularTimeoutHideButton()"] 441 | Click Element css=[ng-click="slowAngularTimeoutHideButton()"] 442 | Element Should Not Be Visible css=[ng-click="slowAngularTimeoutHideButton()"] 443 | 444 | Set Ignore Implicit Angular Wait ${false} 445 | 446 | Click Button css=[ng-click="slowAngularTimeout()"] 447 | 448 | Click Button css=[ng-click="slowAngularTimeoutHideButton()"] 449 | 450 | Element Should Not Be Visible css=[ng-click="slowAngularTimeoutHideButton()"] 451 | 452 | Angular 2 453 | --------- 454 | Looking at filling in the gap of Angular 2 support. Taking a look at the the current state of Protractor the `waitForAngular function`_ has some code to handle both Angular 1 and Angular 2+ code. Taking this Protractor code and combining it with test javascript code above (where we tested tthe core check printing out only '.' while Angular is busy) we have some asemblance of the Angular 1 and Angular 2+ support. 455 | 456 | .. code :: javascript 457 | 458 | var callback = function () {console.log('*')}; 459 | var el = document.querySelector('[ng-app]'); 460 | var h = setInterval(function w4ng() { 461 | console.log('.'); 462 | try { 463 | if (window.angular && !(window.angular.version && 464 | window.angular.version.major > 1)) { 465 | /* ng1 */ 466 | angular.element(el).injector().get('$browser'). 467 | notifyWhenNoOutstandingRequests(callback); 468 | } else if (window.angular.getTestability) { 469 | window.angular.getTestability(el).whenStable(callback); 470 | } 471 | } catch (err) { 472 | console.log(err.message); 473 | callback(err.message); 474 | } 475 | }, 10); 476 | 477 | Some important notes on running this script. Since I wrote the above portions of this write-up Firebug has ceased development and it has been combined with Firefox's developer tools. Under Firefox 53 (my current version) console.log when used within the console prompt no longer outputs to the console. [Yes it returns 'undefined' which is well explained out there and is perfectly valid but not very user friendly]. Chrome on the other hand does. So for now you will need to run the above code in the console within Chrome's dev tools. The other issue is a matter of the getTestibility function and its parent object. It appears that with the Angular development this method has been moved in the object tree and renamed. Under Protractor this function is now window.getAngularTestability. While investigating I was using several test sites. The testapp within Protractor does has a Angular 2 version although greatly simplified over the Angular 1 version. Due to some complications of Chrome, running on a VM, limited RAM, building the ng2 testapp with node, etc. I simplified my investigation by using other test sites. I tried angular.io's `tutorial example`_ but was slightly problimatic. It also is Angular ver 1.6.3 ?!? which isn't very helpful. I settled upon simply the angular.io site - although ... I am realizing many of these Angular site are still Angular 1. 478 | 479 | Ok, this may explain the difference between window.angular.getTestability and window.getAngularTestability. The prior was introduced and available a while back and back in the Angular 1. I was simply lazy in that for my work the notifyWhenNoOutstandingRequests was sufficient. It could be that window.getAngularTestability is purely Angular 2+. ... [Researching] ... Ok form a very brief look, ok one site, it looks like this is the case. Let me put forth what I am thinking 480 | 481 | .. code :: javascript 482 | 483 | var callback = function () {console.log('*')}; 484 | var el = document.querySelector('[ng-app]'); 485 | var h = setInterval(function w4ng() { 486 | console.log('.'); 487 | try { 488 | if (window.angular && !(window.angular.version && 489 | window.angular.version.major > 1)) { 490 | /* ng1 */ 491 | angular.element(el).injector().get('$browser'). 492 | notifyWhenNoOutstandingRequests(callback); 493 | } else if (window.getAngularTestability) { 494 | window.getAngularTestability(el).whenStable(callback); 495 | } else if (window.getAllAngularTestabilities) { 496 | var testabilities = window.getAllAngularTestabilities(); 497 | var count = testabilities.length; 498 | var decrement = function() { 499 | count--; 500 | if (count === 0) { 501 | callback(); 502 | } 503 | }; 504 | testabilities.forEach(function(testability) { 505 | testability.whenStable(decrement); 506 | }); 507 | } else if (!window.angular) { 508 | throw new Error('window.angular is undefined. This could be either ' + 509 | 'because this is a non-angular page or because your test involves ' + 510 | 'client-side navigation. Currently the AngularJS Library is not ' + 511 | 'designed to wait in such situations. Instead you should explicitly ' + 512 | 'call the \'Wait For Angular\' keyword.'); 513 | } else if (window.angular.version >= 2) { 514 | throw new Error('You appear to be using angular, but window.' + 515 | 'getAngularTestability was never set. This may be due to bad ' + 516 | 'obfuscation.'); 517 | } else { 518 | throw new Error('Cannot get testability API for unknown angular ' + 519 | 'version "' + window.angular.version + '"'); 520 | } 521 | } catch (err) { 522 | console.log(err.message); 523 | callback(err.message); 524 | } 525 | }, 10); 526 | 527 | which one could compared with the full Protractor code 528 | 529 | .. code :: javascript 530 | 531 | if (window.angular && !(window.angular.version && 532 | window.angular.version.major > 1)) { 533 | /* ng1 */ 534 | var hooks = getNg1Hooks(rootSelector); 535 | if (hooks.$$testability) { 536 | hooks.$$testability.whenStable(callback); 537 | } else if (hooks.$injector) { 538 | hooks.$injector.get('$browser'). 539 | notifyWhenNoOutstandingRequests(callback); 540 | } else if (!!rootSelector) { 541 | throw new Error('Could not automatically find injector on page: "' + 542 | window.location.toString() + '". Consider using config.rootEl'); 543 | } else { 544 | throw new Error('root element (' + rootSelector + ') has no injector.' + 545 | ' this may mean it is not inside ng-app.'); 546 | } 547 | } else if (rootSelector && window.getAngularTestability) { 548 | var el = document.querySelector(rootSelector); 549 | window.getAngularTestability(el).whenStable(callback); 550 | } else if (window.getAllAngularTestabilities) { 551 | var testabilities = window.getAllAngularTestabilities(); 552 | var count = testabilities.length; 553 | var decrement = function() { 554 | count--; 555 | if (count === 0) { 556 | callback(); 557 | } 558 | }; 559 | testabilities.forEach(function(testability) { 560 | testability.whenStable(decrement); 561 | }); 562 | } else if (!window.angular) { 563 | throw new Error('window.angular is undefined. This could be either ' + 564 | 'because this is a non-angular page or because your test involves ' + 565 | 'client-side navigation, which can interfere with Protractor\'s ' + 566 | 'bootstrapping. See http://git.io/v4gXM for details'); 567 | } else if (window.angular.version >= 2) { 568 | throw new Error('You appear to be using angular, but window.' + 569 | 'getAngularTestability was never set. This may be due to bad ' + 570 | 'obfuscation.'); 571 | } else { 572 | throw new Error('Cannot get testability API for unknown angular ' + 573 | 'version "' + window.angular.version + '"'); 574 | } 575 | 576 | The biggest difference is the simplification of the Angular 1 code. I could simply add the window.angular.getTestability check. For now I am going to move forward with this and then we can revisit this code. 577 | 578 | Execute Async vs Sync Script 579 | ---------------------------- 580 | 581 | So I am coming around to revisiting my decision to use the syncronous javascript call. Reviewing my reasons again was 1) it worked - that is, syncronous worked where my initial attempts with asycronous failed and then 2) I wanted to control loop (i.e. the eventual timeout) to be within Python and 3) I understood what it was doing and, well, it worked. As previously noted I grasped the basic concept of asycronous javascript but I couldn't envision it from the Python call side nor was I fully understanding the implementation of the callback. Since then several events are pushing me forward with, at least, trying out an asyncronous version. Those events include a good conversation with the very wise Marvin Ojwang at the spring 2017 Selenium Conference and some issues I have had with an in-house Angluar sites. Also I have been thinking about the python async call and think I have a better understanding as well as a way of actually showing the async call work with python. 582 | 583 | For the async demo I need as the core a Javascript "sleep" command. I am using the solution provide by Dan Dascalescu `in this stackoverflow posting`_. Note this will work for latest version of Edge, Firefox, and Chrome but not IE11. Also note if you run this within the developer tools (F12) console you will not get the console.log output. I must admit this is both frustrating and, although technical correct, absolutly wrong and useless. If someone wants to log let them log whether from within the console itself or from an internal script or even a selenium execute script call. That sai here is the javascript 584 | 585 | .. code :: javascript 586 | 587 | function sleep(ms) { 588 | return new Promise(resolve => setTimeout(resolve, ms)); 589 | } 590 | 591 | async function demo() { 592 | console.log('Taking a break...'); 593 | await sleep(2000); 594 | console.log('Two second later'); 595 | } 596 | 597 | demo(); 598 | 599 | If you really want to run this in the console window you can change console.log to alert. Just remember that you need to dismiss the alert dialog to complete the initial 'Taking a call...' statement and have the script procede. As we really want to work within Python let's move this there... 600 | 601 | .. code :: pycon 602 | 603 | >>> from selenium import webdriver 604 | >>> driver = webdriver.Firefox() 605 | >>> js="""function sleep(ms){return new Promise(resolve=>setTimeout(resolve,ms))} 606 | ... async function demo(){console.log('Taking a break...');await sleep(2000);console.log('Two second later')} 607 | ... demo()""" 608 | >>> driver.execute_script(js) 609 | >>> 610 | 611 | Now I know this is not the form I want to create for an asyncronous call but let's try this out anyways and see what happens... 612 | 613 | .. code :: pycon 614 | 615 | >>> driver.execute_async_script(js) 616 | Traceback (most recent call last): 617 | File "", line 1, in 618 | File "/home/emanlove/angular/clean-python27-env/local/lib/python2.7/site-packages/selenium/webdriver/remote/webdriver.py", line 553, in execute_async_script 619 | 'args': converted_args})['value'] 620 | File "/home/emanlove/angular/clean-python27-env/local/lib/python2.7/site-packages/selenium/webdriver/remote/webdriver.py", line 297, in execute 621 | self.error_handler.check_response(response) 622 | File "/home/emanlove/angular/clean-python27-env/local/lib/python2.7/site-packages/selenium/webdriver/remote/errorhandler.py", line 194, in check_response 623 | raise exception_class(message, screen, stacktrace) 624 | selenium.common.exceptions.TimeoutException: Message: Timed out 625 | 626 | >>> 627 | 628 | Well it works and then ... we seemingly don't return from the method .. till we get the Timed out message. Ok this is good for a couple reasons. One it should be expected as, first, there is no return (and no callback) and second I need to eventually work out and explain how an internal library timeout interacts with the selenium timeouts. So this is confirmation and a reminder. For kicks and curiosity I am going to try a return value. I'll note I really think the issue with the async script is not the lack of a return value but the missing callback. Changing the script to 629 | 630 | .. code :: pycon 631 | 632 | >>> js_w_return="""function sleep(ms){return new Promise(resolve=>setTimeout(resolve,ms))} 633 | ... async function demo(){console.log('Taking a break...');await sleep(2000);console.log('Two second later')} 634 | ... demo(); 635 | ... return undefined;""" 636 | >>> val=driver.execute_script(js_w_return) 637 | >>> val is None 638 | True 639 | >>> driver.execute_async_script(js_w_return) 640 | Traceback (most recent call last): 641 | File "", line 1, in 642 | File "/home/emanlove/angular/clean-python27-env/local/lib/python2.7/site-packages/selenium/webdriver/remote/webdriver.py", line 553, in execute_async_script 643 | 'args': converted_args})['value'] 644 | File "/home/emanlove/angular/clean-python27-env/local/lib/python2.7/site-packages/selenium/webdriver/remote/webdriver.py", line 297, in execute 645 | self.error_handler.check_response(response) 646 | File "/home/emanlove/angular/clean-python27-env/local/lib/python2.7/site-packages/selenium/webdriver/remote/errorhandler.py", line 194, in check_response 647 | raise exception_class(message, screen, stacktrace) 648 | selenium.common.exceptions.TimeoutException: Message: Timed out 649 | 650 | >>> 651 | 652 | A few observations. The return value had no effect on the async call. Also, not noticed before nor called out but the sync script returns immediately while the script is still running within the test browser. Yes expected, but still interesting none the less. The last observation was that undefined value in Javascript translates to None within Python. I hadn't really thought about this till I didn't see a return value which I'll admit was a little unexpected here. I wanted to confirm I got a return value and the python interpretor on return None does "nothing". I modified the script to return a Javascript false instead, 653 | 654 | .. code :: python 655 | 656 | js_w_bool="""function sleep(ms){return new Promise(resolve=>setTimeout(resolve,ms))} 657 | async function demo(){console.log('Taking a break...');await sleep(2000);console.log('Two second later')} 658 | demo(); 659 | return false;""" 660 | 661 | saw False return value, and then went back and tested the undefined val against None. Minor observation but still important in confirming my understanding and observations. 662 | 663 | Thinking I recalled that the last arguement is supposed to be the callback function which will be called when the async javascript script is completed, I tried something like 664 | 665 | .. code :: pycon 666 | 667 | >>> js="""function sleep(ms){return new Promise(resolve=>setTimeout(resolve,ms))} 668 | ... async function demo(){console.log('Taking a break...');await sleep(2000);console.log('Two second later')} 669 | ... demo();""" 670 | >>> val=driver.execute_script(js,"console.log(); return undefined;") 671 | 672 | But providing what I thought was the callback function didn't work so I went back to the Selenium unit tests. Here we see, as an example, this async call 673 | 674 | .. code :: pycon 675 | 676 | >>> driver.execute_async_script("arguments[arguments.length - 1](123);") 677 | 123 678 | 679 | noting that this function returns without the time exception which will be noteworthy here shortly. Trying to resolve where my thinking about execute_async_script came from I re-disovered `this stackoverflow post`_ which i think was the source for my statement "the last arguement is supposed to be the callback function which will be called when the async javascript script is completed". Honestly, re-reading this post just confuses me again. So I have a basic understanding as arguments and we actually use them in the library because certain characters cause issues within a script and pone solution is to pass them as arguments. Wanting to understand this beeter and thinking about the selenium unit test above I tried the following 680 | 681 | .. code :: pycon 682 | 683 | >>> driver.execute_async_script("console.log(arguments.length)") 684 | 1 685 | >>> driver.execute_async_script("console.log(arguments[0])") 686 | function () 687 | >>> driver.execute_async_script("console.log(arguments[0].toSource())") 688 | rv => __webDriverCallback(rv) 689 | >>> driver.execute_async_script("console.log(arguments[0].toString())") 690 | rv => __webDriverCallback(rv) 691 | >>> 692 | 693 | All of these timed out, by the way which was surprising and interesting. Also the response for the toSouce/toDString function is interesting. What gets me confused about the stackflow article is that I have been thinking of the execute_async_script as the javascript function which may be incorrect. The arguments, the javascript scripts that are been passed into the python method, are not going into a execute_async_script function but may be run within some other function. Trying 694 | 695 | .. code :: pycon 696 | 697 | >>> driver.execute_async_script("var funcName=arguments.callee.toString(); console.log(funcName)") 698 | 699 | results in the following console output 700 | 701 | .. code 702 | 703 | function() { var funcName=arguments.callee.toString(); console.log(funcName) } 704 | 705 | This just re-enforces my confusion. "Without an additional argument passed to the python execute_async_script method, Argument[0] is the javascript function itself one wishes to execute. Thus statements like driver.execute_async_script("arguments[arguments.length - 1](123);") are just some sort of bad circlular function call." 706 | 707 | Funny. As I explore this more I tried the following 708 | 709 | .. code :: pycon 710 | 711 | >>> driver.execute_async_script("var funcName=arguments[0].callee.toString(); console.log(funcName)") 712 | Traceback (most recent call last): 713 | File "", line 1, in 714 | File "/home/emanlove/angular/clean-python27-env/local/lib/python2.7/site-packages/selenium/webdriver/remote/webdriver.py", line 553, in execute_async_script 715 | 'args': converted_args})['value'] 716 | File "/home/emanlove/angular/clean-python27-env/local/lib/python2.7/site-packages/selenium/webdriver/remote/webdriver.py", line 297, in execute 717 | self.error_handler.check_response(response) 718 | File "/home/emanlove/angular/clean-python27-env/local/lib/python2.7/site-packages/selenium/webdriver/remote/errorhandler.py", line 194, in check_response 719 | raise exception_class(message, screen, stacktrace) 720 | selenium.common.exceptions.WebDriverException: Message: TypeError: arguments[0].callee is undefined 721 | 722 | >>> driver.execute_async_script("var funcName=arguments[0]().callee.toString(); console.log(funcName)") 723 | >>> 724 | 725 | The first call above throws an error, as expected, because callee is not is not a function of the first agrument but of arguments. Arguments[0](), on the other hand, is a function and thus should have the callee child object. But when we make this second call the function returns without a timeout and the console log has no output. See a bad recursive function call! 726 | 727 | I had a chance to walk away an forget about this question for a short time. Coming back I am asking whether I can essentially fit the async example into the javascript sleep example. Here is code that says yes we can, 728 | 729 | .. code :: pycon 730 | 731 | >>> from selenium import webdriver 732 | >>> driver=webdriver.Firefox() 733 | >>> js="""var done = arguments[0]; 734 | ... function sleep(ms){return new Promise(resolve=>setTimeout(resolve,ms))} 735 | ... async function demo(){console.log('Taking a break...');await sleep(2000);done('Two second later')} 736 | ... demo()""" 737 | >>> driver.execute_async_script(js) 738 | u'Two second later' 739 | >>> 740 | 741 | Jumping over to our specific goal of asking Angular whether or not it is "busy", trying ... 742 | 743 | .. code :: pycon 744 | 745 | >>> from selenium import webdriver 746 | >>> driver=webdriver.Firefox() 747 | >>> driver.get("http://angular.github.io/angular-phonecat/step-14/app/#!/phones") 748 | >>> js_wait_for_angularjs = """ 749 | ... var callback = arguments[0]; 750 | ... var el = document.querySelector('[ng-app]'); 751 | ... if (typeof angular.element(el).injector() == "undefined") { 752 | ... throw new Error('root element ([ng-app]) has no injector.' + 753 | ... ' this may mean it is not inside ng-app.'); 754 | ... } 755 | ... angular.element(el).injector().get('$browser'). 756 | ... notifyWhenNoOutstandingRequests(callback); 757 | ... """ 758 | >>> driver.execute_async_script(js_wait_for_angularjs) 759 | >>> el=driver.find_elements_by_xpath("//select")[0] 760 | >>> sel=webdriver.support.select.Select(el) 761 | >>> sel.select_by_value('age');driver.execute_async_script(js_wait_for_angularjs); 762 | >>> sel.select_by_value('name');driver.execute_async_script(js_wait_for_angularjs); 763 | >>> sel.select_by_value('age');driver.execute_async_script(js_wait_for_angularjs); 764 | >>> 765 | 766 | These last few lines give us a very unscientific but visual sanity check making sure that we are indeed waiting for angular to complete. 767 | 768 | [... more to come... Need to implement an all javascript version wait for angular up to some give up timeout. Also want to talk the merits of both async and sync solutions and demostrate a very busy angular/javascript test case and show what happens when a blocking call is made.] 769 | 770 | One question I have is with the model above can we send back some value in the sense that we are not wanting to call the arguments[0](some_return_var) function but instead pass it as the callback and when the notifyWhenNoOutstandingRequests function completes then call the callback and return the value. 771 | 772 | 773 | Footnotes 774 | --------- 775 | 776 | [1] Ok, not entirely true. I understand WebDriverJS and Protractor is asycronious javascript and thus when one makes a call to a function you may get the response back in some unknown amount of time or asycronously. Fine. But that is not what I want here. I want an answer back now telling me whether or not the current (now) state of Angular has any outstanding promises or whether or not it is waiting, right now. Don't delay. Thus I am making, conscientiously, a syncronous javascript call and then "waiting" or polling within the AngularJSlibrary. 777 | 778 | [2] The statement that "whenever Protractor looks for an element" may not be entitrely true. If you read the code the waitForAngular call is when you are looking for "all" elements, as in ``element.all(by.id('notPresentElementID'))``. [See `protractor/lib/element.ts`_]. It is unclear, to me without spending a lot more time tracing through the code atleast, that a call to say ``element(by.binding('username'))``, for example, would go through element.all and thus be invoking the implicit wait for angular. 779 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """An AngularJS/Angular extension to Robotframework's SeleniumLibrary 2 | 3 | See: 4 | http://robotframework.org/ 5 | https://github.com/MarketSquare/robotframework-angularjs 6 | """ 7 | 8 | from setuptools import setup, find_packages 9 | from codecs import open 10 | from os import path 11 | 12 | here = path.abspath(path.dirname(__file__)) 13 | 14 | # Get the long description from the README file 15 | with open(path.join(here, 'README.rst'), encoding='utf-8') as f: 16 | long_description = f.read() 17 | 18 | setup( 19 | name='robotframework-angularjs', 20 | version='1.0.0', 21 | description="""An AngularJS/Angular extension to Robotframework's SeleniumLibrary""", 22 | long_description=long_description, 23 | long_description_content_type='text/x-rst', 24 | url='https://github.com/MarketSquare/robotframework-angularjs', 25 | author='Zephraph, Ed Manlove', 26 | author_email='emanlove@verizon.net', 27 | license='Apache License 2.0', 28 | classifiers=[ 29 | 'Development Status :: 6 - Mature', 30 | 'Framework :: Robot Framework', 31 | 'License :: OSI Approved :: Apache Software License', 32 | 'Operating System :: OS Independent', 33 | 'Programming Language :: Python', 34 | 'Programming Language :: Python :: 3', 35 | 'Topic :: Software Development :: Testing', 36 | ], 37 | keywords='robotframework testing testautomation angular selenium webdriver', 38 | packages=find_packages(exclude=['docs']), 39 | install_requires=['robotframework', 'robotframework-seleniumlibrary'], 40 | ) 41 | --------------------------------------------------------------------------------