packages feed

webdriver-angular (empty) → 0.1.0

raw patch · 11 files changed

+958/−0 lines, 11 filesdep +HUnitdep +aesondep +basesetup-changed

Dependencies added: HUnit, aeson, base, hspec, language-javascript, lifted-base, template-haskell, text, transformers, unordered-containers, wai-app-static, warp, webdriver, webdriver-angular

Files

+ LICENSE view
@@ -0,0 +1,20 @@+Copyright (c) 2013 John Lenz++Permission is hereby granted, free of charge, to any person obtaining+a copy of this software and associated documentation files (the+"Software"), to deal in the Software without restriction, including+without limitation the rights to use, copy, modify, merge, publish,+distribute, sublicense, and/or sell copies of the Software, and to+permit persons to whom the Software is furnished to do so, subject to+the following conditions:++The above copyright notice and this permission notice shall be+included in all copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ README.md view
@@ -0,0 +1,7 @@+For end to end testing of web applications from Haskell, the+[webdriver](https://hackage.haskell.org/package/webdriver) package is a great tool but does not+provide specific commands to make testing a webpage using [AngularJs](http://angularjs.org/) easier.+The [protractor](https://github.com/angular/protractor) project provides Angular-specific webdriver+commands but the test code must be written in javascript.  This package fills the gap by reusing+some of the protractor code to allow end to end tests of Angular applications to be written in+Haskell.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ js/angular-clientsidescripts.js view
@@ -0,0 +1,377 @@+/* Copied from https://github.com/angular/protractor without modification */+/* The MIT License++Copyright (c) 2010-2013 Google, Inc.++Permission is hereby granted, free of charge, to any person obtaining a copy+of this software and associated documentation files (the "Software"), to deal+in the Software without restriction, including without limitation the rights+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell+copies of the Software, and to permit persons to whom the Software is+furnished to do so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in+all copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN+THE SOFTWARE.*/++/**+ * All scripts to be run on the client via executeAsyncScript or+ * 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.+ */+var clientSideScripts = exports;++/**+ * Wait until Angular has finished rendering and has+ * no outstanding $http calls before continuing.+ *+ * Asynchronous.+ * arguments[0] {string} The selector housing an ng-app+ * arguments[1] {function} callback+ */+clientSideScripts.waitForAngular = function() {+  var el = document.querySelector(arguments[0]);+  var callback = arguments[1];+  try {+    angular.element(el).injector().get('$browser').+        notifyWhenNoOutstandingRequests(callback);+  } catch (e) {+    callback(e);+  }+};++/**+ * 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}}.+ *+ * @return {Array.<WebElement>} The elements containing the binding.+ */+clientSideScripts.findBindings = function() {+  var using = arguments[0] || document;+  var binding = arguments[1];+  var bindings = using.getElementsByClassName('ng-binding');+  var matches = [];+  for (var i = 0; i < bindings.length; ++i) {+    var elemData  = angular.element(bindings[i]).data();+    if (!elemData || !elemData.$binding) {+      continue;+    }+    var bindingName = elemData.$binding[0].exp || elemData.$binding;+    if (bindingName.indexOf(binding) != -1) {+      matches.push(bindings[i]);+    }+  }+  return matches; // Return the whole array for webdriver.findElements.+};++/**+ * Find an array of elements matching a row within an ng-repeat. Always returns+ * an array of only one element.+ *+ * 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.+ *+ * @return {Array.<Element>} An array of a single element, the row of the+ *     repeater.+ */+ clientSideScripts.findRepeaterRows = function() {+  var using = arguments[0] || document;+  var repeater = arguments[1];+  var index = arguments[2];++  var rows = [];+  var prefixes = ['ng-', 'ng_', 'data-ng-', 'x-ng-', 'ng\\:'];+  for (var p = 0; p < prefixes.length; ++p) {+    var attr = prefixes[p] + 'repeat';+    var repeatElems = using.querySelectorAll('[' + attr + ']');+    attr = attr.replace(/\\/g, '');+    for (var i = 0; i < repeatElems.length; ++i) {+      if (repeatElems[i].getAttribute(attr).indexOf(repeater) != -1) {+        rows.push(repeatElems[i]);+      }+    }+  }+  return [rows[index]];+ };++ /**+ * 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'.+ *+ * @return {Array.<Element>} All rows of the repeater.+ */+ clientSideScripts.findAllRepeaterRows = function() {+  var using = arguments[0] || document;+  var repeater = arguments[1];++  var rows = [];+  var prefixes = ['ng-', 'ng_', 'data-ng-', 'x-ng-', 'ng\\:'];+  for (var p = 0; p < prefixes.length; ++p) {+    var attr = prefixes[p] + 'repeat';+    var repeatElems = using.querySelectorAll('[' + attr + ']');+    attr = attr.replace(/\\/g, '');+    for (var i = 0; i < repeatElems.length; ++i) {+      if (repeatElems[i].getAttribute(attr).indexOf(repeater) != -1) {+        rows.push(repeatElems[i]);+      }+    }+  }+  return rows;+ };++/**+ * 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}}'.+ *+ * @return {Array.<Element>} The element in an array.+ */+clientSideScripts.findRepeaterElement = function() {+  var matches = [];+  var using = arguments[0] || document;+  var repeater = arguments[1];+  var index = arguments[2];+  var binding = arguments[3];++  var rows = [];+  var prefixes = ['ng-', 'ng_', 'data-ng-', 'x-ng-', 'ng\\:'];+  for (var p = 0; p < prefixes.length; ++p) {+    var attr = prefixes[p] + 'repeat';+    var repeatElems = using.querySelectorAll('[' + attr + ']');+    attr = attr.replace(/\\/g, '');+    for (var i = 0; i < repeatElems.length; ++i) {+      if (repeatElems[i].getAttribute(attr).indexOf(repeater) != -1) {+        rows.push(repeatElems[i]);+      }+    }+  }+  var row = rows[index];+  var bindings = [];+  if (row.className.indexOf('ng-binding') != -1) {+    bindings.push(row);+  }+  var childBindings = row.getElementsByClassName('ng-binding');+  for (var i = 0; i < childBindings.length; ++i) {+    bindings.push(childBindings[i]);+  }+  for (var i = 0; i < bindings.length; ++i) {+    var elemData  = angular.element(bindings[i]).data();+    if (!elemData || !elemData.$binding) {+      continue;+    }+    var bindingName = elemData.$binding[0].exp || elemData.$binding;+    if (bindingName.indexOf(binding) != -1) {+      matches.push(bindings[i]);+    }+  }+  return matches;+};++/**+ * 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}}'.+ *+ * @return {Array.<Element>} The elements in the column.+ */+clientSideScripts.findRepeaterColumn = function() {+  var matches = [];+  var using = arguments[0] || document;+  var repeater = arguments[1];+  var binding = arguments[2];++  var rows = [];+  var prefixes = ['ng-', 'ng_', 'data-ng-', 'x-ng-', 'ng\\:'];+  for (var p = 0; p < prefixes.length; ++p) {+    var attr = prefixes[p] + 'repeat';+    var repeatElems = using.querySelectorAll('[' + attr + ']');+    attr = attr.replace(/\\/g, '');+    for (var i = 0; i < repeatElems.length; ++i) {+      if (repeatElems[i].getAttribute(attr).indexOf(repeater) != -1) {+        rows.push(repeatElems[i]);+      }+    }+  }+  for (var i = 0; i < rows.length; ++i) {+    var bindings = [];+    if (rows[i].className.indexOf('ng-binding') != -1) {+      bindings.push(rows[i]);+    }+    var childBindings = rows[i].getElementsByClassName('ng-binding');+    for (var k = 0; k < childBindings.length; ++k) {+      bindings.push(childBindings[k]);+    }+    for (var j = 0; j < bindings.length; ++j) {+      var elemData  = angular.element(bindings[j]).data();+      if (!elemData || !elemData.$binding) {+        continue;+      }+      var bindingName = elemData.$binding[0].exp || elemData.$binding;+      if (bindingName.indexOf(binding) != -1) {+        matches.push(bindings[j]);+      }+    }+  }+  return matches;+};++/**+ * Find an input elements by model name.+ *+ * arguments[0] {Element} The scope of the search.+ * arguments[1] {string} The model name.+ *+ * @return {Array.<Element>} The matching input elements.+ */+clientSideScripts.findInputs = function() {+  var using = arguments[0] || document;+  var model = arguments[1];+  var prefixes = ['ng-', 'ng_', 'data-ng-', 'x-ng-', 'ng\\:'];+  for (var p = 0; p < prefixes.length; ++p) {+    var selector = 'input[' + prefixes[p] + 'model="' + model + '"]';+    var inputs = using.querySelectorAll(selector);+    if (inputs.length) {+      return inputs;+    }+  }+};++/**+ * Find multiple select elements by model name.+ *+ * arguments[0] {Element} The scope of the search.+ * arguments[1] {string} The model name.+ *+ * @return {Array.<Element>} The matching select elements.+ */+clientSideScripts.findSelects = function() {+  var using = arguments[0] || document;+  var model = arguments[1];+  var prefixes = ['ng-', 'ng_', 'data-ng-', 'x-ng-', 'ng\\:'];+  for (var p = 0; p < prefixes.length; ++p) {+    var selector = 'select[' + prefixes[p] + 'model="' + model + '"]';+    var inputs = using.querySelectorAll(selector);+    if (inputs.length) {+      return inputs;+    }+  }+};++/**+ * Find selected option elements by model name.+ *+ * arguments[0] {Element} The scope of the search.+ * arguments[1] {string} The model name.+ *+ * @return {Array.<Element>} The matching select elements.+*/+clientSideScripts.findSelectedOptions = function() {+  var using = arguments[0] || document;+  var model = arguments[1];+  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';+    var inputs = using.querySelectorAll(selector);+    if (inputs.length) {+      return inputs;+    }+  }+};++/**+ * Find textarea elements by model name.+ *+ * arguments[0] {Element} The scope of the search.+ * arguments[1] {String} The model name.+ *+ * @return {Array.<Element>} An array of matching textarea elements.+*/+clientSideScripts.findTextareas = function() {+  var using = arguments[0] || document;+  var model = arguments[1];++  var prefixes = ['ng-', 'ng_', 'data-ng-', 'x-ng-', 'ng\\:'];+  for (var p = 0; p < prefixes.length; ++p) {+    var selector = 'textarea[' + prefixes[p] + 'model="' + model + '"]';+    var textareas = using.querySelectorAll(selector);+    if (textareas.length) {+      return textareas;+    }+  }+};++/**+ * 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+ */+clientSideScripts.testForAngular = function() {+  var attempts = arguments[0];+  var callback = arguments[arguments.length - 1];+  var check = function(n) {+    try {+      if (window.angular && window.angular.resumeBootstrap) {+        callback([true, null]);+      } else if (n < 1) {+        if (window.angular) {+          callback([false, 'angular never provided resumeBootstrap']);+        } else {+          callback([false, 'retries looking for angular exceeded']);+        }+      } else {+        window.setTimeout(function() {check(n - 1)}, 1000);+      }+    } catch (e) {+      callback([false, e]);+    }+  };+  check(attempts);+};++/**+ * 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.+ *+ * @return {?Object} The result of the evaluation.+ */+clientSideScripts.evaluate = function() {+  var element = arguments[0];+  var expression = arguments[1];++  return angular.element(element).scope().$eval(expression);+};++/**+ * Return the current url using $location.absUrl().+ *+ * arguments[0] {string} The selector housing an ng-app+ */+clientSideScripts.getLocationAbsUrl = function() {+  var el = document.querySelector(arguments[0]);+  return angular.element(el).injector().get('$location').absUrl();+};
+ src/Test/WebDriver/Commands/Angular.hs view
@@ -0,0 +1,168 @@+{-# LANGUAGE TemplateHaskell, OverloadedStrings, DeriveDataTypeable #-}+-- | This module exposes <https://hackage.haskell.org/package/webdriver webdriver> actions that can+-- be used to interact with a page which uses <http://angularjs.org/ AngularJs>.  This provides+-- similar functionality as <https://github.com/angular/protractor protractor> and in fact we share+-- some code with protractor.+module Test.WebDriver.Commands.Angular (+    -- * Loading+      waitForAngular++    -- * Searching for elements+    , NgException(..)+    , NgSelector(..)+    , findNg+    , findNgs+    , findNgFrom+    , findNgsFrom+    , NgRepeater(..)+    , findRepeater+    , findRepeaters+    , findRepeaterFrom+    , findRepeatersFrom++    -- * Misc+    , ngEvaluate+    , getLocationAbsUrl+    ) where++import Control.Monad.IO.Class (liftIO, MonadIO)+import Control.Exception (throwIO, Exception)+import Control.Applicative ((<$>))+import Data.Typeable (Typeable)+import Test.WebDriver.Classes+import Test.WebDriver.Commands+import Test.WebDriver.JSON (fromJSON')+import Test.WebDriver.Commands.Internal (clientScripts)+import Language.Haskell.TH (runIO, litE, stringL)+import qualified Data.Aeson as A+import qualified Data.HashMap.Lazy as M+import qualified Data.Text as T++-- | Map of the clientsidescripts for angular+cs :: M.HashMap T.Text T.Text+cs = either (\err -> error $ "Error parsing scripts " ++ err) id mhash+    where+        mhash = clientScripts j+        j = $(runIO (readFile "js/angular-clientsidescripts.js") >>= litE . stringL)++execCS :: (WebDriver wd, A.FromJSON a) => T.Text -> [JSArg] -> wd a+execCS script arg = executeJS arg body+    where+        body = maybe (error $ "Unable to find " ++ T.unpack script) id $ M.lookup script cs++-- | Variant of execCS that fails properly on Null+execElems :: WebDriver wd => T.Text -> [JSArg] -> wd [Element]+execElems script arg = do+    x <- execCS script arg+    case (x, A.fromJSON x) of+        (A.Null, _) -> return []+        (A.Array _, A.Success [A.Null]) -> return []+        _ -> fromJSON' x++asyncCS :: (WebDriver wd, A.FromJSON a) => T.Text -> [JSArg] -> wd (Maybe a)+asyncCS script arg = asyncJS arg body+    where+        body = maybe (error $ "Unable to find " ++ T.unpack script) id $ M.lookup script cs++-- | Wait until Angular has finished rendering before continuing.  @False@ indicates the timeout+-- was hit (see 'setScriptTimeout') and we stopped waiting and @True@ means that angular has+-- finished rendering.+waitForAngular :: WebDriver wd => T.Text -- ^ CSS selector to element which has ng-app+                               -> wd Bool+waitForAngular sel = do+    a <- asyncCS "waitForAngular" [JSArg sel] :: WebDriver wd => wd (Maybe ())+    return $ maybe False (const True) a++-- | Exceptions of this type will be thrown when an element is unable to be located.+data NgException = NgException String+  deriving (Show, Eq, Typeable)+instance Exception NgException++checkOne :: (Show s, MonadIO wd, WebDriver wd) => s -> [Element] -> wd Element+checkOne _ [e] = return e+checkOne sel es = liftIO $ throwIO err+    where+        err = NgException $ "Selector " ++ show sel ++ " returned " ++ show es++data NgSelector = +    ByBinding T.Text -- ^ Argument is the binding, e.g. {{dog.name}}+  | ByModel T.Text   -- ^ Argument is the model name.  This is just a combination of 'ByInput', 'ByTextarea', and 'BySelect'+  | ByInput T.Text   -- ^ Input element with matching model name, e.g. @\<input ng-model=\"name\" ...\>@.+  | ByTextarea T.Text -- ^ Textarea element with matching model name, e.g. @\<textarea ng-model=\"name\" ... \>@+  | BySelect T.Text   -- ^ Select element with matching model name, e.g. @\<select ng-model=\"name\" ... \>@+  | BySelectedOption T.Text -- ^ Selected options with a select element matching the modelname.  That is,+                          --   The @\<option:checked\>@ elements within a @\<select ng-model=\"name\" ... \>@.+  deriving (Show,Eq)++data NgRepeater =+    ByRows T.Text    -- ^ All the rows matching the repeater (e.g. 'dog in dogs')+  | ByRow T.Text Int -- ^ A single row specified by the text of the repeater (e.g. 'dog in dogs') and the row index+  | ByColumn T.Text T.Text -- ^ A single column matching the text of the repeater (e.g. 'dog in dogs') and the+                         -- column binding (e.g '{{dog.name}}').+  | ByRowAndCol T.Text Int T.Text -- ^ A single row and column, given (repeater, row index, column binding).+  deriving (Show,Eq)++-- | Find a single element from the document matching the given Angular selector.  If zero or more+-- than one element is returned, an exception of type 'NgException' is thrown.+findNg :: (MonadIO wd, WebDriver wd) => NgSelector -> wd Element+findNg s = checkOne s =<< findNg' (JSArg A.Null) s++-- | Find elements from the document matching the given Angular selector.+findNgs :: WebDriver wd => NgSelector -> wd [Element]+findNgs = findNg' $ JSArg A.Null++-- | Find a single element from within the given element which matches the given Angular selector. If+-- zero or more than one element is returned, an exception of type 'NgException' is thrown.+findNgFrom :: (MonadIO wd, WebDriver wd) => Element -> NgSelector -> wd Element+findNgFrom e s = checkOne s =<< findNg' (JSArg e) s++-- | Find elements from within the given element which match the given Angular selector.+findNgsFrom :: WebDriver wd => Element -> NgSelector -> wd [Element]+findNgsFrom e = findNg' $ JSArg e++findNg' :: WebDriver wd => JSArg -> NgSelector -> wd [Element]+findNg' e (ByBinding name) = execElems "findBindings" [e, JSArg name]+findNg' e (ByInput name) = execElems "findInputs" [e, JSArg name]+findNg' e (ByTextarea name) = execElems "findTextareas" [e, JSArg name]+findNg' e (BySelect name) = execElems "findSelects" [e, JSArg name]+findNg' e (BySelectedOption name) = execElems "findSelectedOptions" [e, JSArg name]+findNg' e (ByModel name) = concat <$> sequence [ findNg' e $ ByInput name+                                               , findNg' e $ ByTextarea name+                                               , findNg' e $ BySelect name+                                               ]++-- | 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.+findRepeater :: (MonadIO wd, WebDriver wd) => NgRepeater -> wd Element+findRepeater r = checkOne r =<< findRepeater' (JSArg A.Null) r++-- | Find elements from the document which match the Angular repeater.+findRepeaters :: WebDriver wd => NgRepeater -> wd [Element]+findRepeaters = findRepeater' $ JSArg A.Null++-- | Find an element from the given element which matches the Angular repeater.  If zero or more than+-- one are returned, an exception of type 'NgException' is thrown.+findRepeaterFrom :: (MonadIO wd, WebDriver wd) => Element -> NgRepeater -> wd Element+findRepeaterFrom e r = checkOne r =<< findRepeater' (JSArg e) r++-- | Find elements from the given element which match the Angular repeater.+findRepeatersFrom :: WebDriver wd => Element -> NgRepeater -> wd [Element]+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]++-- | Evaluate an angular expression, using the scope attached to the given element.+ngEvaluate :: (WebDriver wd, A.FromJSON a) +           => Element -- ^ element in whose scope to evaluate+           -> T.Text  -- ^ expression to evaluate, e.g. \"dog.name | reverse\"+           -> wd a+ngEvaluate e expr = execCS "evaluate" [JSArg e, JSArg expr]++-- | Return the current absolute url according to Angular (using @$location.absUrl()@).+getLocationAbsUrl :: WebDriver wd => T.Text -- ^ CSS selector to element which has ng-app+                                  -> wd T.Text+getLocationAbsUrl sel = execCS "getLocationAbsUrl" [JSArg sel]
+ src/Test/WebDriver/Commands/Internal.hs view
@@ -0,0 +1,43 @@+{-# LANGUAGE OverloadedStrings, DeriveDataTypeable, GeneralizedNewtypeDeriving  #-}+-- |Internal functions+module Test.WebDriver.Commands.Internal+   ( clientScripts ) where++import Control.Applicative+import Data.Maybe (catMaybes)+import Language.JavaScript.Parser (JSNode(..), Node(..))+import qualified Data.HashMap.Lazy as M+import qualified Data.Text as T+import qualified Language.JavaScript.Parser as JS+++-- | Parse top level javascript commands+parseClientTop :: JSNode -> [(T.Text,T.Text)]+parseClientTop (NN (JSSourceElementsTop es)) = catMaybes $ map parseClientDef es+parseClientTop _ = []++-- | Parse a call clientSideScripts.somefunction = function() {...}, returning the function name+-- and the body.+parseClientDef :: JSNode -> Maybe (T.Text, T.Text)+parseClientDef (NN (JSExpression+                        [NN (JSMemberDot+                            [NT (JSIdentifier "clientSideScripts") _ _]+                            _ -- literal .+                            (NT (JSIdentifier name) _ _))+                        , NN (JSOperator (NT (JSLiteral "=") _ _))+                        , NN (JSFunctionExpression _ _ _ _ _ (NN (JSBlock _ body _)))+                        ]))+    = Just (T.pack name, T.pack $ dropWhile (=='\n') $ JS.renderToString $ NN $ JSSourceElementsTop body)+                        -- Use renderToString instead of renderJS so we don't need to depend on builder+parseClientDef _ = Nothing++-- | Parse a javascript file.  All toplevel commands which look like+--+-- >clientSideScripts.somefunction = function() {+-- > ...+-- >};+--+-- are loaded.  The return value is either an error message or+-- a map with keys the function names and value the body of the function.+clientScripts :: String -> Either String (M.HashMap T.Text T.Text)+clientScripts j = M.fromList . parseClientTop <$> JS.parse j "<client>"
+ test/Specs.hs view
@@ -0,0 +1,204 @@+{-# LANGUAGE OverloadedStrings #-}+module Specs where++import Test.WebDriver.Commands+import Test.WebDriver.Commands.Angular++import Data.Monoid+import Control.Monad.IO.Class (liftIO)+import Control.Exception.Lifted (try, Exception)+import Test.Hspec.Core (Example(..), Result(..))+import Test.HUnit (assertEqual, assertFailure)+import Test.Hspec hiding (shouldReturn, shouldBe, shouldSatisfy, shouldThrow)+import qualified Test.Hspec as H+import qualified Test.WebDriver as W+import qualified Test.WebDriver.Classes as W+import qualified Data.Text as T++data MySession = Firefox+            -- | Chrome++sessionCaps :: MySession -> W.Capabilities+sessionCaps Firefox = W.defaultCaps++matchingCaps :: MySession -> W.Capabilities -> Bool+matchingCaps Firefox (W.Capabilities { W.browser = W.Firefox _ _ _ }) = True+matchingCaps _ _ = False++data WithSession = WithSession MySession (W.WD ())+instance Example WithSession where+    evaluateExample (WithSession stype w) _ action = action (W.runWD session w') >> return Success+        where+            session = W.defaultSession+            w' = do ss <- sessions+                    case filter (\(_, caps) -> matchingCaps stype caps) ss of+                        ((s, _):_) -> W.putSession $ session { W.wdSessId = Just s }+                        _ -> do s <- createSession $ sessionCaps stype+                                W.putSession s+                    w+                        +shouldBe :: (Show a, Eq a) => a -> a -> W.WD ()+x `shouldBe` y = liftIO $ x `H.shouldBe` y++shouldBeTag :: Element -> T.Text -> W.WD ()+e `shouldBeTag` name = do+    t <- tagName e+    liftIO $ assertEqual ("tag of " ++ show e) name t++shouldHaveText :: Element -> T.Text -> W.WD ()+e `shouldHaveText` txt = do+    t <- getText e+    liftIO $ assertEqual ("text of " ++ show e) txt t++shouldHaveAttr :: Element -> (T.Text, T.Text) -> W.WD ()+e `shouldHaveAttr` (a, txt) = do+    t <- attr e a+    liftIO $ assertEqual ("attribute " ++ T.unpack a ++ " of " ++ show e) (Just txt) t++shouldReturn :: (Show a, Eq a) => W.WD a -> a -> W.WD ()+action `shouldReturn` expected = action >>= (\a -> liftIO $ a `H.shouldBe` expected)++shouldThrow :: (Show e, Eq e, Exception e) => W.WD a -> e -> W.WD ()+shouldThrow w expected = do+    r <- try w+    case r of+        Left err -> err `shouldBe` expected+        Right _ -> liftIO $ assertFailure $ "did not get expected exception " ++ show expected++assertName :: T.Text -> W.WD ()+assertName n = do+    e <- findNg $ ByBinding "{{name}}"+    e `shouldBeTag` "h2"+    e `shouldHaveText` ("The name is " <> n)++specs :: Spec+specs = describe "Angular webdriver commands" $ do+    it "finds elements by binding" $ WithSession Firefox $ do+        openPage "http://localhost:3456/index.html"+        _ <- waitForAngular "body" --`shouldReturn` True++        binding <- findNg $ ByBinding "{{a}}"+        binding `shouldBeTag` "h1"+        binding `shouldHaveText` "Hello A!"++    it "finds input elements" $ WithSession Firefox $ do+        i <- findNg $ ByInput "name"+        i `shouldBeTag` "input"+        sendKeys "John" i+        assertName "John"++        i2 <- findNg $ ByInput "xxx"+        i2 `shouldBeTag` "input"++        findNgs (ByInput "yyy") `shouldReturn` []+        findNg (ByInput "yyy") `shouldThrow` NgException "Selector ByInput \"yyy\" returned []"++    it "finds textarea elements" $ WithSession Firefox $ do+        openPage "http://localhost:3456/index.html"+        _ <- waitForAngular "body" --`shouldReturn` True++        t <- findNg $ ByTextarea "name"+        t `shouldBeTag` "textarea"+        sendKeys "Mark" t+        assertName "Mark"++        t2 <- findNg $ ByTextarea "yyy"+        t2 `shouldBeTag` "textarea"+        sendKeys "Bob" t2+        assertName "Mark"++        findNgs (ByTextarea "xxx") `shouldReturn` []+        findNg (ByTextarea "xxx") `shouldThrow` NgException "Selector ByTextarea \"xxx\" returned []"++    it "finds select elements" $ WithSession Firefox $ do+        s <- findNg $ BySelect "name"+        s `shouldBeTag` "select"+        opt <- findElemFrom s $ ByCSS "option[value=\"Goodbye\"]"+        click opt+        assertName "Goodbye"++        opt' <- findNg $ BySelectedOption "name"+        opt' `shouldBeTag` "option"+        opt' `shouldHaveText` "Goodbye"++        findNgs (BySelect "xxx") `shouldReturn` []++    it "finds model elements of different types" $ WithSession Firefox $ do+        [i, t, s] <- findNgs $ ByModel "name"+        i `shouldBeTag` "input"+        t `shouldBeTag` "textarea"+        s `shouldBeTag` "select"+        i `shouldHaveAttr` ("value", "Goodbye")+        t `shouldHaveAttr` ("value", "Goodbye")++    it "finds a single module element" $ WithSession Firefox $ do+        i <- findNg $ ByModel "xxx"+        i `shouldBeTag` "input"+        i `shouldHaveText` ""++        t <- findNg $ ByModel "yyy"+        t `shouldBeTag` "textarea"++        s <- findNg $ ByModel "zzz"+        s `shouldBeTag` "select"++    it "finds all repeater rows" $ WithSession Firefox $ do+        [r1, r2, r3] <- findRepeaters $ ByRows "dog in dogs"+        mapM_ (`shouldBeTag`"li") [r1, r2, r3]+        r1 `shouldHaveText` "Spot mutt"+        r2 `shouldHaveText` "Spike poodle"+        r3 `shouldHaveText` "Jupiter bulldog"++        findRepeaters (ByRows "cat in cats") `shouldReturn` []+        findRepeater (ByRows "cat in cats") `shouldThrow` NgException "Selector ByRows \"cat in cats\" returned []"++    it "finds a single repeater row" $ WithSession Firefox $ do+        r2 <- findRepeater $ ByRow "dog in dogs" 1+        r2 `shouldBeTag` "li"+        r2 `shouldHaveText` "Spike poodle"++        findRepeaters (ByRow "cat in cats" 1) `shouldReturn` []+        findRepeater (ByRow "cat in cats" 1) `shouldThrow` NgException "Selector ByRow \"cat in cats\" 1 returned []"++    it "finds a repeater column" $ WithSession Firefox $ do+        [c1, c2, c3] <- findRepeaters $ ByColumn "dog in dogs" "{{dog.name}}"+        mapM_ (`shouldBeTag`"span") [c1, c2, c3]++        c1 `shouldHaveText` "Spot"+        c2 `shouldHaveText` "Spike"+        c3 `shouldHaveText` "Jupiter"++        findRepeaters (ByColumn "cat in cats" "{{cat.name}}") `shouldReturn` []+        findRepeater (ByColumn "cat in cats" "{{cat.name}}") `shouldThrow`+            NgException "Selector ByColumn \"cat in cats\" \"{{cat.name}}\" returned []"++    it "finds a repeater by row and column" $ WithSession Firefox $ do+        c2 <- findRepeater $ ByRowAndCol "dog in dogs" 1 "{{dog.breed}}"+        c2 `shouldBeTag` "span"+        c2 `shouldHaveText` "poodle"++        -- These currently cause Javascript errors+        --findRepeaters (ByRowAndCol "cat in cats" 12 "{{cat.name}}") `shouldReturn` []+        --findRepeater (ByRowAndCol "cat in cats" 22 "{{cat.name}}") `shouldThrow`+        --    NgException "Selector ByRowAndCol \"cat in cats\" 22 \"{{cat.name}}\" returned []"++    it "evaluates an angular expression" $ WithSession Firefox $ do+        e <- findNg $ ByBinding "{{a}}"+        ngEvaluate e "cost | number:2" `shouldReturn` ("12.60" :: T.Text)++    it "loads the location url" $ WithSession Firefox $ do+        getLocationAbsUrl "body" `shouldReturn` "http://localhost:3456/index.html"++    it "loads {{cost}} from document" $ WithSession Firefox $ do+        [s1, s2] <- findNgs $ ByBinding "{{cost}}"+        s1 `shouldBeTag` "span"+        s2 `shouldBeTag` "span"+        s1 `shouldHaveAttr` ("id", "span-one")+        s2 `shouldHaveAttr` ("id", "span-two")++    it "loads {{cost}} only from one element" $ WithSession Firefox $ do+        d <- findElem $ ById "one"+        d `shouldBeTag` "div"+        s1 <- findNgFrom d $ ByBinding "{{cost}}"+        s1 `shouldBeTag` "span"+        s1 `shouldHaveAttr` ("id", "span-one")
+ test/main.hs view
@@ -0,0 +1,26 @@+{-# LANGUAGE OverloadedStrings #-}+module Main where++import Control.Concurrent+import Control.Exception (bracket)+import Network.Wai.Application.Static+import Network.Wai.Handler.Warp (run)+import Test.Hspec (hspec)++import Specs++startServer :: IO ThreadId+startServer = forkIO $+    run 3456 $ staticApp $ defaultFileServerSettings "test/www"++{-stop :: ThreadId -> IO ()+stop t = do+    runWD defaultSession $ do+        ss <- sessions+        mapM_ (\(x,_) -> putSession defaultSession {wdSessId = Just x} >> closeSession) ss++    killThread t-}++main :: IO ()+main = bracket startServer killThread $ \_ ->+    hspec specs
+ test/www/index.html view
@@ -0,0 +1,37 @@+<!doctype html>+<html>+    <head>+        <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.5/angular.min.js"></script>+        <script src="ng.js"></script>+    </head>+    <body ng-app="test">+        <div ng-controller="TestCtrl">+            <h1>Hello {{a}}!</h1>+            <h2>The name is {{name}}</h2>+            <input ng-model="name"></input>+            <input ng-model="xxx"></input>+            <textarea ng-model="name"></textarea>+            <textarea ng-model="yyy"></textarea>+            <select ng-model="name">+                    <option value="Hello">Hello</option>+                    <option value="Goodbye">Goodbye</option>+            </select>+            <select ng-model="zzz">+                    <option value="Foo">Foo</option>+                    <option value="Bar">Bar</option>+            </select>+            <ul>+                <li ng-repeat="dog in dogs">+                    <span>{{dog.name}}</span>+                    <span>{{dog.breed}}</span>+                </li>+            </ul>+            <div id="one">+                <span id="span-one">{{cost}}</span>+            </div>+            <div id="two">+                <span id="span-two">{{cost}}</span>+            </div>+        </div>+    </body>+</html>
+ test/www/ng.js view
@@ -0,0 +1,8 @@+angular.module("test", [])+.controller("TestCtrl", function($scope) {+    $scope.a = "A";+    $scope.dogs = [{name:"Spot", breed:"mutt"},+                   {name:"Spike", breed:"poodle"},+                   {name:"Jupiter", breed:"bulldog"}];+    $scope.cost = 12.6;+});
+ webdriver-angular.cabal view
@@ -0,0 +1,66 @@+name:              webdriver-angular+version:           0.1.0+cabal-version:     >= 1.8+build-type:        Simple+synopsis:          Webdriver actions to assist with testing a webpage which uses Angular.Js+category:          Web+author:            John Lenz <lenz@math.uic.edu>+maintainer:        John Lenz <lenz@math.uic.edu>+license:           MIT+license-file:      LICENSE+homepage:          https://bitbucket.org/wuzzeb/hs-webdriver-angular+stability:         Experimental+description:       For end to end testing of web applications from Haskell, the +                   <https://hackage.haskell.org/package/webdriver webdriver> package is a great tool but+                   does not provide specific commands to make testing a webpage using +                   <http://angularjs.org/ AngularJS> easier.  The +                   <https://github.com/angular/protractor protractor> project provides Angular-specific+                   webdriver commands but the test code must be written in javascript.  This package+                   fills the gap by reusing some of the protractor code to allow end to end tests of Angular+                   applications to be written in Haskell.++extra-source-files: js/*.js,+                    README.md,+                    test/*.hs,+                    test/www/*.html,+                    test/www/*.js++source-repository head+    type: mercurial+    location: https://bitbucket.org/wuzzeb/hs-webdriver-angular++library+    hs-source-dirs:  src++    exposed-modules: Test.WebDriver.Commands.Angular++    other-modules:   Test.WebDriver.Commands.Internal++    ghc-options:   -Wall -O2++    build-depends: base                 >= 4          && < 5++                 , webdriver            >= 0.5  && < 0.6+                 , aeson                >= 0.6+                 , language-javascript  >= 0.5  && < 0.6+                 , template-haskell     >= 0.6+                 , text                 >= 0.11 && < 0.12+                 , transformers         >= 0.3+                 , unordered-containers >= 0.2  && < 0.3++test-suite test+    type:            exitcode-stdio-1.0+    main-is:         main.hs+    hs-source-dirs:  test+    ghc-options:     -Wall++    build-depends: base >= 4 && < 5+                 , hspec >= 1.8+                 , HUnit >= 1.2 && < 1.3+                 , lifted-base >= 0.2+                 , text >= 0.11+                 , transformers >= 0.3+                 , warp >= 2.0+                 , wai-app-static >= 2.0+                 , webdriver+                 , webdriver-angular