webdriver-angular 0.1.3 → 0.1.4
raw patch · 5 files changed
+217/−100 lines, 5 files
Files
- js/angular-clientsidescripts.js +182/−90
- src/Test/WebDriver/Commands/Angular.hs +20/−7
- src/Test/WebDriver/Commands/Internal.hs +10/−2
- test/NgSpec.hs +4/−0
- webdriver-angular.cabal +1/−1
js/angular-clientsidescripts.js view
@@ -26,9 +26,15 @@ * executeScript should be put here. These scripts are transmitted over * the wire using their toString representation, and cannot reference * external variables. They can, however use the array passed in to- * arguments. Instead of params, all functions on clientSideScripts- * should list the arguments array they expect.+ * arguments.+ *+ * Some implementations seem to have issues with // comments, so use star-style+ * inside scripts. */++ // jshint browser: true+ // jshint shadow: true+ /* global angular */ var clientSideScripts = exports; /**@@ -36,12 +42,12 @@ * no outstanding $http calls before continuing. * * Asynchronous.- * arguments[0] {string} The selector housing an ng-app- * arguments[1] {function} callback+ *+ * @param {string} selector The selector housing an ng-app+ * @param {function} callback callback */-clientSideScripts.waitForAngular = function() {- var el = document.querySelector(arguments[0]);- var callback = arguments[1];+clientSideScripts.waitForAngular = function(selector, callback) {+ var el = document.querySelector(selector); try { angular.element(el).injector().get('$browser'). notifyWhenNoOutstandingRequests(callback);@@ -53,14 +59,13 @@ /** * Find a list of elements in the page by their angular binding. *- * arguments[0] {Element} The scope of the search.- * arguments[1] {string} The binding, e.g. {{cat.name}}.+ * @param {string} binding The binding, e.g. {{cat.name}}.+ * @param {Element} using The scope of the search. *- * @return {Array.<WebElement>} The elements containing the binding.+ * @return {Array.<Element>} The elements containing the binding. */-clientSideScripts.findBindings = function() {- var using = arguments[0] || document;- var binding = arguments[1];+clientSideScripts.findBindings = function(binding, using) {+ using = using || document; var bindings = using.getElementsByClassName('ng-binding'); var matches = []; for (var i = 0; i < bindings.length; ++i) {@@ -72,7 +77,7 @@ } } }- return matches; // Return the whole array for webdriver.findElements.+ return matches; /* Return the whole array for webdriver.findElements. */ }; /**@@ -80,17 +85,15 @@ * Always returns an array of only one element for plain old ng-repeat. * Returns an array of all the elements in one segment for ng-repeat-start. *- * arguments[0] {Element} The scope of the search.- * arguments[1] {string} The text of the repeater, e.g. 'cat in cats'.- * arguments[2] {number} The row index.+ * @param {string} repeater The text of the repeater, e.g. 'cat in cats'.+ * @param {number} index The row index.+ * @param {Element} using The scope of the search. Defaults to 'document'. * * @return {Array.<Element>} The row of the repeater, or an array of elements * in the first row in the case of ng-repeat-start. */- clientSideScripts.findRepeaterRows = function() {- var using = arguments[0] || document;- var repeater = arguments[1];- var index = arguments[2];+ clientSideScripts.findRepeaterRows = function(repeater, index, using) {+ using = using || document; var prefixes = ['ng-', 'ng_', 'data-ng-', 'x-ng-', 'ng\\:']; var rows = [];@@ -104,8 +107,8 @@ } } }- // multiRows is an array of arrays, where each inner array contains- // one row of elements.+ /* multiRows is an array of arrays, where each inner array contains+ one row of elements. */ var multiRows = []; for (var p = 0; p < prefixes.length; ++p) { var attr = prefixes[p] + 'repeat-start';@@ -132,14 +135,13 @@ /** * Find all rows of an ng-repeat. *- * arguments[0] {Element} The scope of the search.- * arguments[1] {string} The text of the repeater, e.g. 'cat in cats'.+ * @param {string} repeater The text of the repeater, e.g. 'cat in cats'.+ * @param {Element} using The scope of the search. Defaults to 'document'. * * @return {Array.<Element>} All rows of the repeater. */- clientSideScripts.findAllRepeaterRows = function() {- var using = arguments[0] || document;- var repeater = arguments[1];+ clientSideScripts.findAllRepeaterRows = function(repeater, using) {+ using = using || document; var rows = []; var prefixes = ['ng-', 'ng_', 'data-ng-', 'x-ng-', 'ng\\:'];@@ -160,7 +162,6 @@ for (var i = 0; i < repeatElems.length; ++i) { if (repeatElems[i].getAttribute(attr).indexOf(repeater) != -1) { var elem = repeatElems[i];- var row = []; while (elem.nodeType != 8 || elem.nodeValue.indexOf(repeater) == -1) { rows.push(elem);@@ -175,19 +176,16 @@ /** * Find an element within an ng-repeat by its row and column. *- * arguments[0] {Element} The scope of the search.- * arguments[1] {string} The text of the repeater, e.g. 'cat in cats'.- * arguments[2] {number} The row index.- * arguments[3] {string} The column binding, e.g. '{{cat.name}}'.+ * @param {string} repeater The text of the repeater, e.g. 'cat in cats'.+ * @param {number} index The row index.+ * @param {string} binding The column binding, e.g. '{{cat.name}}'.+ * @param {Element} using The scope of the search. Defaults to 'document'. * * @return {Array.<Element>} The element in an array. */-clientSideScripts.findRepeaterElement = function() {+clientSideScripts.findRepeaterElement = function(repeater, index, binding, using) { var matches = [];- var using = arguments[0] || document;- var repeater = arguments[1];- var index = arguments[2];- var binding = arguments[3];+ using = using || document; var rows = []; var prefixes = ['ng-', 'ng_', 'data-ng-', 'x-ng-', 'ng\\:'];@@ -201,8 +199,8 @@ } } }- // multiRows is an array of arrays, where each inner array contains- // one row of elements.+ /* multiRows is an array of arrays, where each inner array contains+ one row of elements. */ var multiRows = []; for (var p = 0; p < prefixes.length; ++p) { var attr = prefixes[p] + 'repeat-start';@@ -237,7 +235,7 @@ } if (multiRow) { for (var i = 0; i < multiRow.length; ++i) {- rowElem = multiRow[i];+ var rowElem = multiRow[i]; if (rowElem.className.indexOf('ng-binding') != -1) { bindings.push(rowElem); }@@ -262,17 +260,15 @@ /** * Find the elements in a column of an ng-repeat. *- * arguments[0] {Element} The scope of the search.- * arguments[1] {string} The text of the repeater, e.g. 'cat in cats'.- * arguments[2] {string} The column binding, e.g. '{{cat.name}}'.+ * @param {string} repeater The text of the repeater, e.g. 'cat in cats'.+ * @param {string} binding The column binding, e.g. '{{cat.name}}'.+ * @param {Element} using The scope of the search. Defaults to 'document'. * * @return {Array.<Element>} The elements in the column. */-clientSideScripts.findRepeaterColumn = function() {+clientSideScripts.findRepeaterColumn = function(repeater, binding, using) { var matches = [];- var using = arguments[0] || document;- var repeater = arguments[1];- var binding = arguments[2];+ using = using || document; var rows = []; var prefixes = ['ng-', 'ng_', 'data-ng-', 'x-ng-', 'ng\\:'];@@ -286,8 +282,8 @@ } } }- // multiRows is an array of arrays, where each inner array contains- // one row of elements.+ /* multiRows is an array of arrays, where each inner array contains+ one row of elements. */ var multiRows = []; for (var p = 0; p < prefixes.length; ++p) { var attr = prefixes[p] + 'repeat-start';@@ -346,14 +342,13 @@ * Find an input elements by model name. * DEPRECATED - use findByModel *- * arguments[0] {Element} The scope of the search.- * arguments[1] {string} The model name.+ * @param {string} model The model name.+ * @param {Element} using The scope of the search. Defaults to 'document'. * * @return {Array.<Element>} The matching input elements. */-clientSideScripts.findInputs = function() {- var using = arguments[0] || document;- var model = arguments[1];+clientSideScripts.findInputs = function(model, using) {+ using = using || document; var prefixes = ['ng-', 'ng_', 'data-ng-', 'x-ng-', 'ng\\:']; for (var p = 0; p < prefixes.length; ++p) { var selector = 'input[' + prefixes[p] + 'model="' + model + '"]';@@ -365,16 +360,15 @@ }; /**- * Find a elements by model name.+ * Find elements by model name. *- * arguments[0] {Element} The scope of the search.- * arguments[1] {string} The model name.+ * @param {string} model The model name.+ * @param {Element} using The scope of the search. Defaults to 'document'. * * @return {Array.<Element>} The matching elements. */-clientSideScripts.findByModel = function() {- var using = arguments[0] || document;- var model = arguments[1];+clientSideScripts.findByModel = function(model, using) {+ using = using || document; var prefixes = ['ng-', 'ng_', 'data-ng-', 'x-ng-', 'ng\\:']; for (var p = 0; p < prefixes.length; ++p) { var selector = '[' + prefixes[p] + 'model="' + model + '"]';@@ -386,16 +380,72 @@ }; /**+ * Find buttons by textual content.+ *+ * @param {string} searchText The exact text to match.+ * @param {Element} using The scope of the search. Defaults to 'document'.+ *+ * @return {Array.<Element>} The matching elements.+ */+clientSideScripts.findByButtonText = function(searchText, using) {+ using = using || document;+ var elements = using.querySelectorAll('button, input[type="button"], input[type="submit"]');+ var matches = [];+ for (var i = 0; i < elements.length; ++i) {+ var element = elements[i];+ var elementText;+ if (element.tagName.toLowerCase() == 'button') {+ elementText = element.innerText || element.textContent;+ } else {+ elementText = element.value;+ }+ if (elementText === searchText) {+ matches.push(element);+ }+ }++ return matches;+};++/**+ * Find buttons by textual content.+ *+ * @param {string} searchText The exact text to match.+ * @param {Element} using The scope of the search. Defaults to 'document'.+ *+ * @return {Array.<Element>} The matching elements.+ */+clientSideScripts.findByPartialButtonText = function(searchText, using) {+ using = using || document;+ var elements = using.querySelectorAll('button, input[type="button"], input[type="submit"]');+ var matches = [];+ for (var i = 0; i < elements.length; ++i) {+ var element = elements[i];+ var elementText;+ if (element.tagName.toLowerCase() == 'button') {+ elementText = element.innerText || element.textContent;+ } else {+ elementText = element.value;+ }+ if (elementText.indexOf(searchText) > -1) {+ matches.push(element);+ }+ }++ return matches;+};+++/** * Find multiple select elements by model name. *- * arguments[0] {Element} The scope of the search.- * arguments[1] {string} The model name.+ * @param {string} model The model name.+ * @param {Element} using The scope of the search. Defaults to 'document'. * * @return {Array.<Element>} The matching select elements. */-clientSideScripts.findSelects = function() {- var using = arguments[0] || document;- var model = arguments[1];+clientSideScripts.findSelects = function(model, using) {+ using = using || document; var prefixes = ['ng-', 'ng_', 'data-ng-', 'x-ng-', 'ng\\:']; for (var p = 0; p < prefixes.length; ++p) { var selector = 'select[' + prefixes[p] + 'model="' + model + '"]';@@ -409,14 +459,13 @@ /** * Find selected option elements by model name. *- * arguments[0] {Element} The scope of the search.- * arguments[1] {string} The model name.+ * @param {string} model The model name.+ * @param {Element} using The scope of the search. Defaults to 'document'. * * @return {Array.<Element>} The matching select elements. */-clientSideScripts.findSelectedOptions = function() {- var using = arguments[0] || document;- var model = arguments[1];+clientSideScripts.findSelectedOptions = function(model, using) {+ using = using || document; var prefixes = ['ng-', 'ng_', 'data-ng-', 'x-ng-', 'ng\\:']; for (var p = 0; p < prefixes.length; ++p) { var selector = 'select[' + prefixes[p] + 'model="' + model + '"] option:checked';@@ -430,14 +479,13 @@ /** * Find textarea elements by model name. *- * arguments[0] {Element} The scope of the search.- * arguments[1] {String} The model name.+ * @param {String} model The model name.+ * @param {Element} using The scope of the search. Defaults to 'document'. * * @return {Array.<Element>} An array of matching textarea elements. */-clientSideScripts.findTextareas = function() {- var using = arguments[0] || document;- var model = arguments[1];+clientSideScripts.findTextareas = function(model, using) {+ using = using || document; var prefixes = ['ng-', 'ng_', 'data-ng-', 'x-ng-', 'ng\\:']; for (var p = 0; p < prefixes.length; ++p) {@@ -450,16 +498,43 @@ }; /**+ * Find elements by css selector and textual content.+ *+ * @param {string} cssSelector The css selector to match.+ * @param {string} searchText The exact text to match.+ * @param {Element} using The scope of the search. Defaults to 'document'.+ *+ * @return {Array.<Element>} An array of matching elements.+ */+clientSideScripts.findByCssContainingText = function(cssSelector, searchText, using) {+ var using = using || document;+ var elements = using.querySelectorAll(cssSelector);+ var matches = [];+ for (var i = 0; i < elements.length; ++i) {+ var element = elements[i];+ var elementText = element.innerText || element.textContent;+ if (elementText.indexOf(searchText) > -1) {+ matches.push(element);+ }+ }+ return matches;+};++/** * Tests whether the angular global variable is present on a page. Retries * in case the page is just loading slowly. * * Asynchronous.- * arguments[0] {number} Number of times to retry.- * arguments[1] {function} callback+ *+ * @param {number} attempts Number of times to retry.+ * @param {function} asyncCallback callback */-clientSideScripts.testForAngular = function() {- var attempts = arguments[0];- var callback = arguments[arguments.length - 1];+clientSideScripts.testForAngular = function(attempts, asyncCallback) {+ var callback = function(args) {+ setTimeout(function() {+ asyncCallback(args);+ }, 0);+ }; var check = function(n) { try { if (window.angular && window.angular.resumeBootstrap) {@@ -471,7 +546,7 @@ callback([false, 'retries looking for angular exceeded']); } } else {- window.setTimeout(function() {check(n - 1)}, 1000);+ window.setTimeout(function() {check(n - 1);}, 1000); } } catch (e) { callback([false, e]);@@ -483,14 +558,12 @@ /** * Evalute an Angular expression in the context of a given element. *- * arguments[0] {Element} The element in whose scope to evaluate.- * arguments[1] {string} The expression to evaluate.+ * @param {Element} element The element in whose scope to evaluate.+ * @param {string} expression The expression to evaluate. * * @return {?Object} The result of the evaluation. */-clientSideScripts.evaluate = function() {- var element = arguments[0];- var expression = arguments[1];+clientSideScripts.evaluate = function(element, expression) { return angular.element(element).scope().$eval(expression); };@@ -498,9 +571,28 @@ /** * Return the current url using $location.absUrl(). *- * arguments[0] {string} The selector housing an ng-app+ * @param {string} selector The selector housing an ng-app */-clientSideScripts.getLocationAbsUrl = function() {- var el = document.querySelector(arguments[0]);+clientSideScripts.getLocationAbsUrl = function(selector) {+ var el = document.querySelector(selector); return angular.element(el).injector().get('$location').absUrl();+};++/**+ * Browse to another page using in-page navigation.+ *+ * @param {string} selector The selector housing an ng-app+ * @param {string} url In page URL using the same syntax as $location.url(),+ * /path?search=a&b=c#hash+ */+clientSideScripts.setLocation = function(selector, url) {+ var el = document.querySelector(selector);+ var $injector = angular.element(el).injector();+ var $location = $injector.get('$location');+ var $rootScope = $injector.get('$rootScope');++ if (url !== $location.url()) {+ $location.url(url);+ $rootScope.$digest();+ } };
src/Test/WebDriver/Commands/Angular.hs view
@@ -23,6 +23,7 @@ -- * Misc , ngEvaluate , getLocationAbsUrl+ , setNgLocation ) where import Control.Applicative ((<$>))@@ -125,9 +126,9 @@ findNgsFrom e = findNg' $ JSArg e findNg' :: WebDriver wd => JSArg -> NgSelector -> wd [Element]-findNg' e (ByBinding name) = execElems "findBindings" [e, JSArg name]-findNg' e (ByModel name) = execElems "findByModel" [e, JSArg name]-findNg' e (BySelectedOption name) = execElems "findSelectedOptions" [e, JSArg name]+findNg' e (ByBinding name) = execElems "findBindings" [JSArg name, e]+findNg' e (ByModel name) = execElems "findByModel" [JSArg name, e]+findNg' e (BySelectedOption name) = execElems "findSelectedOptions" [JSArg name, e] -- | Find an element from the document which matches the Angular repeater. If zero or more than one -- element are returned, an exception of type 'NgException' is thrown.@@ -148,10 +149,10 @@ findRepeatersFrom e = findRepeater' $ JSArg e findRepeater' :: WebDriver wd => JSArg -> NgRepeater -> wd [Element]-findRepeater' e (ByRows rep) = execElems "findAllRepeaterRows" [e, JSArg rep]-findRepeater' e (ByRow rep idx) = execElems "findRepeaterRows" [e, JSArg rep, JSArg idx]-findRepeater' e (ByColumn rep idx) = execElems "findRepeaterColumn" [e, JSArg rep, JSArg idx]-findRepeater' e (ByRowAndCol rep row col) = execElems "findRepeaterElement" [e, JSArg rep, JSArg row, JSArg col]+findRepeater' e (ByRows rep) = execElems "findAllRepeaterRows" [JSArg rep, e]+findRepeater' e (ByRow rep idx) = execElems "findRepeaterRows" [JSArg rep, JSArg idx, e]+findRepeater' e (ByColumn rep idx) = execElems "findRepeaterColumn" [JSArg rep, JSArg idx, e]+findRepeater' e (ByRowAndCol rep row col) = execElems "findRepeaterElement" [JSArg rep, JSArg row, JSArg col, e] -- | Evaluate an angular expression, using the scope attached to the given element. ngEvaluate :: (WebDriver wd, A.FromJSON a) @@ -164,3 +165,15 @@ getLocationAbsUrl :: WebDriver wd => T.Text -- ^ CSS selector to element which has ng-app -> wd T.Text getLocationAbsUrl sel = execCS "getLocationAbsUrl" [JSArg sel]++-- | Browse to another page using in-page navigation (via @$location.url@).+setNgLocation :: (MonadIO wd, WebDriver wd)+ => T.Text -- ^ CSS selector to the element which has ng-app+ -> T.Text -- ^ URL+ -> wd ()+setNgLocation sel url = do+ x <- execCS "setLocation" [JSArg sel, JSArg url]+ case x of+ A.Null -> return ()+ _ -> liftIO $ throwIO $ NgException $ "Error setting location: " ++ show x+
src/Test/WebDriver/Commands/Internal.hs view
@@ -5,6 +5,7 @@ import Control.Applicative import Data.Maybe (catMaybes)+import Data.Monoid ((<>)) import Language.JavaScript.Parser (JSNode(..), Node(..)) import qualified Data.HashMap.Lazy as M import qualified Data.Text as T@@ -16,6 +17,13 @@ parseClientTop (NN (JSSourceElementsTop es)) = catMaybes $ map parseClientDef es parseClientTop _ = [] +-- | Build a function body that can be passed to async. The body must use+-- arguments array but the code from clientSideScripts.js uses function arguments.+buildFunction :: Node -> T.Text+buildFunction func = "return (" <> T.pack func' <> ").apply(null, arguments);"+ where+ func' = dropWhile (=='\n') $ JS.renderToString $ NN func+ -- | Parse a call clientSideScripts.somefunction = function() {...}, returning the function name -- and the body. parseClientDef :: JSNode -> Maybe (T.Text, T.Text)@@ -25,9 +33,9 @@ _ -- literal . (NT (JSIdentifier name) _ _)) , NN (JSOperator (NT (JSLiteral "=") _ _))- , NN (JSFunctionExpression _ _ _ _ _ (NN (JSBlock _ body _)))+ , NN (func@(JSFunctionExpression _ _ _ _ _ _)) ]))- = Just (T.pack name, T.pack $ dropWhile (=='\n') $ JS.renderToString $ NN $ JSSourceElementsTop body)+ = Just (T.pack name, buildFunction func) -- Use renderToString instead of renderJS so we don't need to depend on builder parseClientDef _ = Nothing
test/NgSpec.hs view
@@ -117,3 +117,7 @@ s1 <- findNgFrom d $ ByBinding "{{cost}}" s1 `shouldBeTag` "span" s1 `shouldHaveAttr` ("id", "span-one")++ it "sets the location" $ runWD $ do+ setNgLocation "body" "foo"+ getLocationAbsUrl "body" `shouldReturn` "http://localhost:3456/index.html#/foo"
webdriver-angular.cabal view
@@ -1,5 +1,5 @@ name: webdriver-angular-version: 0.1.3+version: 0.1.4 cabal-version: >= 1.8 build-type: Simple synopsis: Webdriver actions to assist with testing a webpage which uses Angular.Js