├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── build.sbt ├── project ├── build.properties ├── headers.sbt └── release.sbt ├── src ├── main │ └── scala │ │ └── com │ │ └── markatta │ │ └── scalenium │ │ ├── Await.scala │ │ ├── Browser.scala │ │ ├── Cookies.scala │ │ ├── Element.scala │ │ ├── ElementInteractions.scala │ │ ├── ElementSeq.scala │ │ ├── FailureHandler.scala │ │ ├── Forms.scala │ │ ├── HasDriver.scala │ │ ├── HasSearchContext.scala │ │ ├── JqueryStyle.scala │ │ ├── MarkupSearch.scala │ │ ├── Navigation.scala │ │ ├── PageProperties.scala │ │ ├── ScreenShots.scala │ │ ├── Scripts.scala │ │ ├── Specs2Integration.scala │ │ ├── Time.scala │ │ └── package.scala └── test │ └── scala │ └── com │ └── markatta │ └── scalenium │ ├── DslSpec.scala │ ├── HttpTestServer.scala │ └── ScaleniumSpec.scala └── version.sbt /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.log 3 | 4 | # sbt specific 5 | dist/* 6 | target/ 7 | lib_managed/ 8 | src_managed/ 9 | project/boot/ 10 | project/plugins/project/ 11 | 12 | # Scala-IDE specific 13 | .scala_dependencies 14 | 15 | # IDEA 16 | .idea 17 | .idea_modules 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: scala 2 | jdk: 3 | - oraclejdk8 4 | scala: 5 | - 2.10.6 6 | - 2.11.4 7 | - 2.12.2 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This software is licensed under the Apache 2 license, quoted below. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this project except in compliance with 4 | the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 5 | 6 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an 7 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific 8 | language governing permissions and limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | scalenium 2 | ========= 3 | [![Build Status](https://travis-ci.org/johanandren/scalenium.svg?branch=master)](https://travis-ci.org/johanandren/scalenium) 4 | 5 | A Scala-ified fluent wrapper for selenium webdriver heavily inspired by FluentLenium but making use 6 | of the scala collection API:s and all the fun DSL:y stuff possible with Scala. 7 | 8 | License: [Apache Licence v2](https://www.apache.org/licenses/LICENSE-2.0.txt). 9 | 10 | 11 | 12 | Using the library 13 | ----------------- 14 | 15 | Scalenium is available from maven central, just add it Scalenium to your dependencies: 16 | 17 | ```scala 18 | libraryDependencies += "com.markatta" %% "scalenium" % "1.0.3" 19 | ``` 20 | 21 | Latest stable version: 22 | 23 | * Scala 2.12 - [![Maven Central, Scala 2.12](https://maven-badges.herokuapp.com/maven-central/com.markatta/scalenium_2.12/badge.svg)](http://search.maven.org/#search%7Cga%7C1%7Ca%3A%22scalenium_2.12%22) 24 | * Scala 2.11 - [![Maven Central, Scala 2.11](https://maven-badges.herokuapp.com/maven-central/com.markatta/scalenium_2.11/badge.svg)](http://search.maven.org/#search%7Cga%7C1%7Ca%3A%22scalenium_2.11%22) 25 | * Scala 2.10 - [![Maven Central, Scala 2.10](https://maven-badges.herokuapp.com/maven-central/com.markatta/scalenium_2.10/badge.svg)](http://search.maven.org/#search%7Cga%7C1%7Ca%3A%22scalenium_2.10%22) 26 | 27 | 28 | 29 | Setting up base object of library 30 | --------------------------------- 31 | 32 | Everything in the API is available through the class com.markatta.scalenium.Browser 33 | 34 | You can create a Browser instance like this (of course you may use any other selenium driver): 35 | ```scala 36 | import com.markatta.scalenium._ 37 | val browser = new Browser(new FirefoxDriver()) 38 | ``` 39 | 40 | Selecting elements in a page 41 | ---------------------------- 42 | 43 | Is done with a css selector, here are some samples of how it would look in specs2 tests 44 | 45 | ```scala 46 | import com.markatta.scalenium._ 47 | val b: Browser = ... 48 | 49 | b.find(".cssClass") // returns a Seq[Element] 50 | b.first(".cssClass") // returns an Option[Element] 51 | 52 | // aliases to find, for an additional style, see the JQuery syntax example below 53 | b.all(".cssClass") 54 | b.select(".cssClass") 55 | 56 | // use regular scala collection API ops 57 | b.find(".someClass").isEmpty must beTrue 58 | b.find(".someClass") must haveSize(0) 59 | b.first(".someClass").isEmpty must beTrue 60 | b.find(".someClass").exists(_.id == "someId") must beTrue 61 | b.find(".someClass").size must equalTo(3) 62 | 63 | // Element contains the same ops as browser for searching so 64 | // these two gives the same result 65 | b.first(".someClass").get.first(".someOtherClass") 66 | b.first(".someClass > .someOtherClass") 67 | 68 | // Seq[Element] is implicitly converted to an ElementSeq by 69 | // com.markatta.scalenium.seqOfElements2ElementSeq 70 | // allowing us to: 71 | 72 | b.find(".cssClass").hidden must beTrue // all elements matching .cssClass hidden 73 | b.find(".someClass > .someOtherClass").anySelected must beTrue 74 | b.find("li").texts must contain("Banana", "Pineapple") 75 | b.find("ul").find("li").size must equalTo(4) 76 | 77 | b.first("#it") must beSome 78 | b.first("#it").map(_.visible) must beSome(true) 79 | ``` 80 | 81 | JQuery style selection 82 | ---------------------- 83 | It is also possible to do selects with a jquery flavored syntax, which will use an implicit browser for searching: 84 | ```scala 85 | import com.markatta.scalenium._ 86 | import JqueryStyle._ 87 | implicit b: Browser = ... 88 | 89 | $("#someId").visible must beTrue 90 | $("ul > li").exists(li => li.text == "Banana" && li.visible) must beTrue 91 | ``` 92 | 93 | Entering data into forms 94 | ------------------------ 95 | ```scala 96 | import com.markatta.scalenium._ 97 | val b: Browser = ... 98 | 99 | b.write("newPassword").into("input[name='password']") 100 | b.write("happy").intoAll("input[type='text']") 101 | 102 | b.first("input[name='password']").get.write("newPassword") 103 | // if multiple elements are matched this writes into all of them 104 | b.find("input[name='password']").write("newPassword") 105 | 106 | 107 | // fill entire form 108 | // identifying fields with name 109 | b.fillByName("user" -> "johndoe", "password" -> "newPasword") 110 | // id 111 | b.fillById("user" -> "johndoe", "password" -> "newPassword") 112 | // or css selector 113 | b.fill("input#user" -> "johndoe", "input[name='password']" -> "newPassword") 114 | ``` 115 | 116 | 117 | Waiting for asynchronous javascript logic 118 | ----------------------------------------- 119 | In many cases we want to wait for some asynchronous operation for a while before 120 | failing a test, the default timeout is 5 seconds, polling every 250ms: 121 | 122 | ```scala 123 | import com.markatta.scalenium._ 124 | val b: Browser = ... 125 | 126 | // to get specs2 test failures instead of exceptions 127 | import Specs2Integration.specs2FailureHandler 128 | 129 | b.waitFor(".someClass").toBecomeVisible() 130 | b.waitAtMost(5).secondsFor(".someClass").toBecomeVisible() 131 | 132 | b.waitFor(1 == 2).toBecomeTrue() 133 | b.waitAtMost(3000000).secondsFor(1 == 2).toBecomeTrue 134 | 135 | b.waitAtMost(5).secondsFor(b.find("button").allAreDisabled).toBecomeTrue 136 | ``` 137 | 138 | The default timeout and polling interval can be provided with implicit values: 139 | ```scala 140 | import com.markatta.scalenium._ 141 | import scala.concurrent.duration._ 142 | 143 | implicit val timeout = Timeout(3.seconds) 144 | implicit val interval = Interval(100.millis) 145 | 146 | b.waitFor(".someClass").toBecomeVisible() 147 | ``` 148 | 149 | Note that you need to mix in NoTimeConversions into your test spec or else the Specs2 time unit classes 150 | will interfere with the scala.concurrent.duration time units. 151 | 152 | Error handling 153 | -------------- 154 | TODO not so sure about this, should the error handling be set on the browser object instead, it will probably not need to change in the same browser?? 155 | 156 | Error handling is done by implicit implementations of the various XxxFailureHandler traits, 157 | the defaults throw exceptions defined in the library which might not suit any given test framework 158 | or your use case so good. Therefore you can define your own or use another set of predefined 159 | failure handlers by defining them as implicits closer to where you use the framework. 160 | 161 | Example 162 | ```scala 163 | // to get specs2 test failures instead of exceptions 164 | import Specs2Integration.specs2FailureHandler // <- defined as an "implicit object" 165 | 166 | ... use API:s ... 167 | ``` 168 | 169 | or 170 | ```scala 171 | implicit val customHandler = new MissingElementFailureHandler { ... } 172 | 173 | ... use API:s ... 174 | ``` 175 | 176 | 177 | License 178 | ------- 179 | Copyright 2012 Johan Andrén 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | http://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. 192 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | import de.heikoseeberger.sbtheader.license.Apache2_0 2 | 3 | name := "scalenium" 4 | organization := "com.markatta" 5 | 6 | scalaVersion := "2.12.2" 7 | crossScalaVersions := Seq(scalaVersion.value, "2.11.11", "2.10.6") 8 | 9 | scalacOptions ++= Seq("-feature", "-deprecation") 10 | 11 | libraryDependencies ++= Seq( 12 | "org.seleniumhq.selenium" % "selenium-java" % "2.44.0", 13 | // this should probably be an optional dependency somehow, extract specs2 integration to separate module? 14 | "org.specs2" %% "specs2" % "2.4.17" 15 | ) 16 | 17 | resolvers ++= Seq( 18 | // required for the scalaz-streams dependency from specs2 :( 19 | "Scalaz Bintray Repo" at "http://dl.bintray.com/scalaz/releases" 20 | ) 21 | 22 | initialCommands := """ 23 | import com.markatta.scalenium._ 24 | val browser = new Browser(new org.openqa.selenium.htmlunit.HtmlUnitDriver) 25 | """ 26 | 27 | headers := Map( 28 | "scala" -> Apache2_0("2015", "Johan Andrén") 29 | ) 30 | 31 | // releasing 32 | sonatypeSettings 33 | licenses := Seq("Apache License, Version 2.0" -> url("http://www.apache.org/licenses/LICENSE-2.0")) 34 | homepage := Some(url("https://github.com/johanandren/scalenium")) 35 | publishMavenStyle := true 36 | publishArtifact in Test := false 37 | pomIncludeRepository := { _ => false } 38 | publishTo := Some { 39 | val nexus = "https://oss.sonatype.org/" 40 | if (isSnapshot.value) 41 | "snapshots" at nexus + "content/repositories/snapshots" 42 | else 43 | "releases" at nexus + "service/local/staging/deploy/maven2" 44 | } 45 | 46 | pomExtra := 47 | 48 | git@github.com:johanandren/scalenium.git 49 | scm:git:git@github.com:johanandren/scalenium.git 50 | 51 | 52 | 53 | johanandren 54 | Johan Andrén 55 | johan@markatta.com 56 | https://markatta.com/johan/codemonkey 57 | 58 | 59 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.16 2 | -------------------------------------------------------------------------------- /project/headers.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("de.heikoseeberger" % "sbt-header" % "1.3.1") -------------------------------------------------------------------------------- /project/release.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "0.2.2") 2 | 3 | addSbtPlugin("com.github.gseitz" % "sbt-release" % "1.0.6") 4 | -------------------------------------------------------------------------------- /src/main/scala/com/markatta/scalenium/Await.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Johan Andrén 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.markatta.scalenium 18 | 19 | import scala.concurrent.duration._ 20 | import scala.annotation.tailrec 21 | 22 | object Await { 23 | 24 | /** Run until check returns true or timeout from now is passed 25 | * @return 26 | */ 27 | private def becameTrue(timeout: Timeout, interval: Interval, check: => Boolean): Boolean = { 28 | val end = System.currentTimeMillis + timeout.inner.toMillis 29 | 30 | @tailrec 31 | def loop(until: Long, interval: FiniteDuration, predicate: => Boolean): Boolean = { 32 | if (System.currentTimeMillis() > end) { 33 | false 34 | } else if (predicate) { 35 | true 36 | } else { 37 | Thread.sleep(interval.toMillis) 38 | loop(until, interval, predicate) 39 | } 40 | } 41 | 42 | loop(end, interval.inner, check) 43 | } 44 | 45 | } 46 | 47 | trait Await { this: MarkupSearch => 48 | import Await._ 49 | 50 | implicit val defaultTimeout = Timeout(5.seconds) 51 | implicit val defaultInterval = Interval(250.millis) 52 | 53 | 54 | class UntilCssSelector(cssSelector: String, timeout: Timeout, pollingInterval: Interval) { 55 | def toBecomeVisible[A]()(implicit failureHandler: TimeoutFailureHandler): Unit = { 56 | if (!becameTrue(timeout, pollingInterval, first(cssSelector).isDefined)) { 57 | failureHandler.fail(timeout, "element matching '" + cssSelector + "' never became visible") 58 | } 59 | } 60 | def toDisappear()(implicit failureHandler: TimeoutFailureHandler): Unit = { 61 | if (!becameTrue(timeout, pollingInterval, first(cssSelector).isEmpty)) { 62 | failureHandler.fail(timeout, "element matching '" + cssSelector + "' never disappeared") 63 | } 64 | } 65 | } 66 | 67 | class UntilPredicate(predicate: => Boolean, timeout: Timeout, pollingInterval: Interval) { 68 | def toBecomeTrue()(implicit failureHandler: TimeoutFailureHandler): Unit = { 69 | if (!becameTrue(timeout, pollingInterval, predicate)) { 70 | failureHandler.fail(timeout, "expression never became true") 71 | } 72 | } 73 | def toBecomeFalse()(implicit failureHandler: TimeoutFailureHandler): Unit = { 74 | if (!becameTrue(timeout, pollingInterval, !predicate)) { 75 | failureHandler.fail(timeout, "expression never became false") 76 | } 77 | } 78 | } 79 | 80 | // implicit timeout 81 | def waitFor(predicate: => Boolean)(implicit timeout: Timeout, interval: Interval) = 82 | new UntilPredicate(predicate, timeout, interval) 83 | 84 | def waitFor(cssSelector: String)(implicit timeout: Timeout, interval: Interval) = 85 | new UntilCssSelector(cssSelector, timeout, interval) 86 | 87 | // explicit timeouts 88 | class WaitAtMostBuilder private[Await](n: Long) { 89 | def secondsFor(predicate: => Boolean)(implicit interval: Interval) = new UntilPredicate(predicate, new Timeout(n.seconds), interval) 90 | def secondsFor(cssSelector: String)(implicit interval: Interval) = new UntilCssSelector(cssSelector, new Timeout(n.seconds), interval) 91 | def msFor(predicate: => Boolean)(implicit interval: Interval) = new UntilPredicate(predicate, new Timeout(n.millis), interval) 92 | def msFor(cssSelector: String)(implicit interval: Interval) = new UntilCssSelector(cssSelector, new Timeout(n.millis), interval) 93 | 94 | } 95 | def waitAtMost(n: Long) = new WaitAtMostBuilder(n) 96 | 97 | } 98 | -------------------------------------------------------------------------------- /src/main/scala/com/markatta/scalenium/Browser.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Johan Andrén 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.markatta.scalenium 18 | 19 | import org.openqa.selenium._ 20 | 21 | object Browser { 22 | def apply(driver: WebDriver): Browser = new Browser(driver) 23 | } 24 | 25 | /** 26 | * Represents the browser, use it to do stuff you do with your browser, but automated. 27 | * @param driver Selenium WebDriver to use as backend 28 | */ 29 | final class Browser(protected val driver: WebDriver) 30 | extends HasDriver 31 | with HasSearchContext 32 | with PageProperties 33 | with Cookies 34 | with ScreenShots 35 | with Navigation 36 | with Scripts 37 | with MarkupSearch 38 | with Await 39 | with Forms { 40 | 41 | override val searchContext = driver 42 | 43 | /** close the current window, quit of no windows left */ 44 | def close(): Unit = { 45 | driver.close() 46 | } 47 | 48 | /** shut down the selenium browser */ 49 | def quit(): Unit = { 50 | driver.quit() 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/main/scala/com/markatta/scalenium/Cookies.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Johan Andrén 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.markatta.scalenium 18 | 19 | import scala.collection.breakOut 20 | import scala.collection.JavaConversions._ 21 | import java.util.Date 22 | 23 | trait Cookies { this: HasDriver => 24 | 25 | def cookies: Map[String, Cookie] = driver.manage().getCookies.map(c => (c.getName, Cookie(c)))(breakOut) 26 | 27 | } 28 | 29 | object Cookie { 30 | def apply(c: org.openqa.selenium.Cookie): Cookie = 31 | new Cookie(c.getName, c.getDomain, c.getPath, c.getExpiry, c.getValue, c.isSecure) 32 | } 33 | case class Cookie(name: String, domain: String, path: String, expiry: Date, value: String, secure: Boolean) 34 | -------------------------------------------------------------------------------- /src/main/scala/com/markatta/scalenium/Element.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Johan Andrén 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.markatta.scalenium 18 | 19 | import org.openqa.selenium.{Dimension, WebElement, WebDriver} 20 | import org.openqa.selenium.interactions.Actions 21 | 22 | final class Element private[scalenium]( 23 | protected val cssSelector: String, 24 | protected val driver: WebDriver, 25 | val webElement: WebElement) 26 | extends HasDriver 27 | with ElementInteractions 28 | with HasSearchContext 29 | with MarkupSearch 30 | with Await { 31 | 32 | override val searchContext = webElement 33 | 34 | /** @return the value of the given attribute name */ 35 | def apply(attributeName: String): String = webElement.getAttribute(attributeName) 36 | 37 | def tagName: String = webElement.getTagName 38 | def value: String = this("value") 39 | def id: String = this("id") 40 | def name: String = this("name") 41 | 42 | def text: String = webElement.getText 43 | 44 | def html: String = this("innerHTML") 45 | 46 | def enabled: Boolean = webElement.isEnabled 47 | def disabled: Boolean = !enabled 48 | 49 | /** @return is the element shown onscreen */ 50 | def visible: Boolean = webElement.isDisplayed 51 | def hidden: Boolean = !visible 52 | 53 | /** is a checkbox or radio button element selected */ 54 | def selected: Boolean = webElement.isSelected 55 | 56 | 57 | /** @return onscreen dimensions of the element */ 58 | def size: Dimension = webElement.getSize 59 | 60 | 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/main/scala/com/markatta/scalenium/ElementInteractions.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Johan Andrén 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.markatta.scalenium 18 | 19 | import org.openqa.selenium.Keys 20 | import org.openqa.selenium.interactions.Actions 21 | 22 | trait ElementInteractions { this: Element => 23 | 24 | 25 | // keyboard interactions 26 | 27 | /** clears the given text/input element and writes the given text in it */ 28 | def write(text: String)(implicit failureHandler: MissingElementFailureHandler): Element = { 29 | operationAsEither { 30 | clear() 31 | webElement.sendKeys(text) 32 | } 33 | } 34 | 35 | /** clear the text content of the element */ 36 | def clear()(implicit failureHandler: MissingElementFailureHandler): Element = { 37 | operationAsEither(webElement.clear()) 38 | } 39 | 40 | /** trigger a key down for the given key on the element */ 41 | def keyDown(key: Keys)(implicit failureHandler: MissingElementFailureHandler): Element = 42 | doAction(_.keyDown(webElement, key)) 43 | 44 | /** trigger a key up for the given key on the element */ 45 | def keyUp(key: Keys)(implicit failureHandler: MissingElementFailureHandler): Element = 46 | doAction(_.keyUp(webElement, key)) 47 | 48 | 49 | 50 | // mouse interactions 51 | 52 | 53 | def click()(implicit failureHandler: MissingElementFailureHandler): Element = 54 | operationAsEither(webElement.click()) 55 | 56 | def clickAndHold()(implicit failureHandler: MissingElementFailureHandler): Element = 57 | doAction(_.clickAndHold(webElement)) 58 | 59 | def contextClick()(implicit failureHandler: MissingElementFailureHandler): Element = 60 | doAction(_.contextClick(webElement)) 61 | 62 | def doubleClick()(implicit failureHandler: MissingElementFailureHandler): Element = 63 | doAction(_.doubleClick(webElement)) 64 | 65 | def dragAndDropTo(other: Element)(implicit failureHandler: MissingElementFailureHandler): Element = 66 | doAction(_.dragAndDrop(webElement, other.webElement)) 67 | 68 | /** move the mouse cursor to above the center of the element */ 69 | def hover()(implicit failureHandler: MissingElementFailureHandler): Element = 70 | doAction(_.moveToElement(webElement)) 71 | 72 | 73 | /** submit the form that the element belongs to, if the element is a form element */ 74 | @deprecated("This only does .click(), just use that instead") 75 | def submit()(implicit failureHandler: MissingElementFailureHandler): Element = { 76 | operationAsEither { 77 | webElement.click() 78 | }(failureHandler) 79 | } 80 | 81 | 82 | private def doAction(builder: Actions => Actions)(implicit failureHandler: MissingElementFailureHandler): Element = { 83 | operationAsEither { 84 | builder(new Actions(driver)).perform() 85 | } 86 | this 87 | } 88 | 89 | private def operationAsEither(b: => Any)(implicit failureHandler: MissingElementFailureHandler): Element = { 90 | try { 91 | b 92 | this 93 | } catch { 94 | case e: NoSuchElementException => 95 | failureHandler.noSuchElement(cssSelector) 96 | // we will never get here because all failure handlers will throw exceptions! 97 | this 98 | } 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /src/main/scala/com/markatta/scalenium/ElementSeq.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Johan Andrén 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.markatta.scalenium 18 | 19 | /** 20 | * Useful operations you might want to perform on a Seq of Element 21 | * 22 | * @param elements decorated seq 23 | */ 24 | final class ElementSeq private[scalenium] (elements: Seq[Element]) { 25 | 26 | /** Click on all elements on the list. 27 | * Only the visible elements are filled, if no elements are visible 28 | * nothing happens. 29 | */ 30 | def click()(implicit failureHandler: MissingElementFailureHandler): Seq[Element] = { 31 | elements.foreach(_.click()) 32 | this.elements 33 | } 34 | 35 | /** Clear all visible elements in the list, 36 | * if no elements are visible 37 | * nothing happens. */ 38 | def clearAll()(implicit failureHandler: MissingElementFailureHandler): Seq[Element] = { 39 | elements.filter(_.visible).foreach(_.clear()) 40 | this.elements 41 | } 42 | 43 | /** write the given text into all elements of the list */ 44 | def write(text: String)(implicit failureHandler: MissingElementFailureHandler): Seq[Element] = { 45 | elements.foreach(_.write(text)) 46 | this.elements 47 | } 48 | 49 | /** @return all element ids */ 50 | def ids: Seq[String] = elements.map(_.id) 51 | 52 | /** @return all element text contents */ 53 | def texts: Seq[String] = elements.map(_.text) 54 | /** @return the text content of all elements concatenated into one string */ 55 | def text: String = elements.map(_.text).mkString 56 | def values: Seq[String] = elements.map(_.value) 57 | def html: String = elements.map(_.html).mkString 58 | 59 | /** @return true if all elements in the seq are enabled */ 60 | def enabled = elements.forall(_.enabled) 61 | /** @return true if all elements in the seq are disabled */ 62 | def disabled = elements.forall(_.disabled) 63 | 64 | def anyEnabled = elements.exists(_.enabled) 65 | def anyDisabled = elements.exists(_.disabled) 66 | 67 | /** @return true if all elements in the sequence are visible */ 68 | def visible: Boolean = elements.forall(_.visible) 69 | /** @return true if all elements in the sequence are hidden */ 70 | def hidden: Boolean = elements.forall(!_.hidden) 71 | 72 | def anyVisible: Boolean = elements.exists(_.visible) 73 | def anyHidden: Boolean = elements.exists(!_.visible) 74 | 75 | def anySelected = elements.exists(_.selected) 76 | 77 | def find(cssSelector: String): Seq[Element] = elements.flatMap(_.find(cssSelector)) 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/main/scala/com/markatta/scalenium/FailureHandler.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Johan Andrén 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.markatta.scalenium 18 | 19 | 20 | /* Allows for customization of how failures are handled to adapt 21 | * the library to a specific testing framework. 22 | */ 23 | 24 | final class MissingElementException(message: String) extends RuntimeException(message) 25 | final class AwaitFailedException(msg: String) extends RuntimeException(msg) 26 | 27 | /** default simple failure handler, will throw an [[com.markatta.scalenium.AwaitFailedException]], 28 | * see [[com.markatta.scalenium.Specs2Integration]] for an example of 29 | * testing framework integration */ 30 | object SimpleFailureHandler extends MissingElementFailureHandler with TimeoutFailureHandler { 31 | 32 | def fail(timeout: Timeout, msg: String): Unit = { 33 | throw new AwaitFailedException("Waited " + Time.humanText(timeout.inner) + " but " + msg) 34 | } 35 | 36 | /** fail with an explanation */ 37 | def noSuchElement(msg: String): Unit = { 38 | throw new MissingElementException(msg) 39 | } 40 | } 41 | 42 | 43 | object MissingElementFailureHandler { 44 | implicit val defaultMissingElementHandler = SimpleFailureHandler 45 | } 46 | 47 | trait MissingElementFailureHandler { 48 | /** 49 | * An element was asked for but not found inside the page 50 | * 51 | * @param elementSelector The css selector that was missing 52 | * @return 53 | */ 54 | def noSuchElement(elementSelector: String): Unit 55 | } 56 | 57 | object TimeoutFailureHandler { 58 | implicit val defaultTimeoutFailureHandler = SimpleFailureHandler 59 | } 60 | 61 | trait TimeoutFailureHandler { 62 | /** 63 | * Some kind of async query timed out, an element was not shown onscreen etc. 64 | * @param timeout 65 | * @param message 66 | * @return 67 | */ 68 | def fail(timeout: Timeout, message: String): Unit 69 | } 70 | -------------------------------------------------------------------------------- /src/main/scala/com/markatta/scalenium/Forms.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Johan Andrén 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.markatta.scalenium 18 | 19 | trait Forms { this: MarkupSearch => 20 | 21 | final class FieldWriter private[Forms](text: String) { 22 | def into(cssSelector: String)(implicit failureHandler: MissingElementFailureHandler): Unit = { 23 | first(cssSelector).map(_.write(text)) 24 | } 25 | 26 | def intoAll(cssSelector: String)(implicit failureHandler: MissingElementFailureHandler): Unit = { 27 | find(cssSelector).foreach(_.write(text)) 28 | } 29 | } 30 | 31 | final def write(text: String): FieldWriter = new FieldWriter(text) 32 | 33 | /** Fill each field by name with the corresponding value 34 | * @param vals name -> value 35 | */ 36 | def fillByName(vals: (String, String)*)(implicit failureHandler: MissingElementFailureHandler): Unit = { 37 | fill(vals, "name['" + _ + "']")(failureHandler) 38 | } 39 | 40 | /** Fill each field by id with the corresponding value 41 | * @param vals id -> value 42 | */ 43 | def fillById(vals: (String, String)*)(implicit failureHandler: MissingElementFailureHandler): Unit = { 44 | fill(vals, "#" + _)(failureHandler) 45 | } 46 | 47 | /** Fill each field by css selector with the corresponding value 48 | * @param vals cssSelector -> value 49 | */ 50 | def fill(vals: (String, String)*)(implicit failureHandler: MissingElementFailureHandler): Unit = { 51 | fill(vals, key => key)(failureHandler) 52 | } 53 | 54 | private def fill(vals: Seq[(String, String)], f: String => String)(failureHandler: MissingElementFailureHandler): Unit = { 55 | vals.foreach { case (key, value) => find(f(key)).write(value)(failureHandler)} 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/scala/com/markatta/scalenium/HasDriver.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Johan Andrén 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.markatta.scalenium 18 | 19 | import org.openqa.selenium.WebDriver 20 | 21 | trait HasDriver { 22 | protected def driver: WebDriver 23 | } 24 | -------------------------------------------------------------------------------- /src/main/scala/com/markatta/scalenium/HasSearchContext.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Johan Andrén 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.markatta.scalenium 18 | 19 | import org.openqa.selenium.SearchContext 20 | 21 | trait HasSearchContext { 22 | 23 | def searchContext: SearchContext 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/scala/com/markatta/scalenium/JqueryStyle.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Johan Andrén 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.markatta.scalenium 18 | 19 | object JqueryStyle { 20 | def $(cssSelector: String)(implicit browser: Browser): Seq[Element] = browser.find(cssSelector) 21 | } 22 | -------------------------------------------------------------------------------- /src/main/scala/com/markatta/scalenium/MarkupSearch.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Johan Andrén 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.markatta.scalenium 18 | 19 | import collection.JavaConversions._ 20 | import org.openqa.selenium._ 21 | import org.openqa.selenium.interactions.Actions 22 | 23 | 24 | trait MarkupSearch { this: HasDriver with HasSearchContext => 25 | 26 | /** @return all matching elements or empty sequence if not found or any other type of error */ 27 | def find(cssSelector: String): Seq[Element] = 28 | try { 29 | searchContext.findElements(By.cssSelector(cssSelector)).map(e => new Element(cssSelector, driver, e)) 30 | } catch { 31 | case e: RuntimeException => Seq() 32 | } 33 | 34 | // aliases for find 35 | def select(cssSelector: String) = find(cssSelector) 36 | def all(cssSelector: String) = find(cssSelector) 37 | 38 | // TODO: figure out a way to keep the selector in the option so that we can say what selection failed on None.get 39 | def first(cssSelector: String): Option[Element] = find(cssSelector).headOption 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/scala/com/markatta/scalenium/Navigation.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Johan Andrén 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.markatta.scalenium 18 | 19 | trait Navigation { this: HasDriver => 20 | 21 | def goTo(url: String): this.type = { 22 | require(url != "", "URL must not be empty") 23 | driver.get(url) 24 | this 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/scala/com/markatta/scalenium/PageProperties.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Johan Andrén 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.markatta.scalenium 18 | 19 | trait PageProperties { this: HasDriver => 20 | 21 | /** @return the title of the current page */ 22 | def pageTitle: String = driver.getTitle 23 | 24 | def currentUrl: String = driver.getCurrentUrl 25 | 26 | def pageSource: String = driver.getPageSource 27 | 28 | def focusedElement: Element = new Element("", driver, driver.switchTo.activeElement()) 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/scala/com/markatta/scalenium/ScreenShots.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Johan Andrén 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.markatta.scalenium 18 | 19 | import org.openqa.selenium.{OutputType, TakesScreenshot} 20 | import java.io.File 21 | import org.apache.commons.io.FileUtils 22 | 23 | trait ScreenShots { this: HasDriver => 24 | 25 | private def defaultScreenShotName = System.currentTimeMillis + ".png" 26 | 27 | def takeScreenShot(fileName: String = defaultScreenShotName): this.type = { 28 | require(fileName != "", "Filename of screenshot cannot be empty") 29 | 30 | // TODO default directory? 31 | val scrFile = driver.asInstanceOf[TakesScreenshot].getScreenshotAs(OutputType.FILE) 32 | try { 33 | val destFile = new File(fileName) 34 | FileUtils.copyFile(scrFile, destFile) 35 | } catch { 36 | case e: RuntimeException => throw new RuntimeException("error when taking the snapshot", e) 37 | } 38 | 39 | this 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/scala/com/markatta/scalenium/Scripts.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Johan Andrén 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.markatta.scalenium 18 | 19 | import org.openqa.selenium.JavascriptExecutor 20 | 21 | trait Scripts { this: HasDriver => 22 | def executeScript(script: String, args: String*): this.type = { 23 | driver match { 24 | case j: JavascriptExecutor => j.executeScript(script, args: _*) 25 | case x => throw new IllegalStateException("Driver " + x + " is not a javascript executor") 26 | } 27 | this 28 | } 29 | 30 | def executeAsyncScript(script: String, args: String*): this.type = { 31 | driver match { 32 | case j: JavascriptExecutor => j.executeAsyncScript(script, args: _*) 33 | case x => throw new IllegalStateException("Driver " + x + " is not a javascript executor") 34 | } 35 | this 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/scala/com/markatta/scalenium/Specs2Integration.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Johan Andrén 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.markatta.scalenium 18 | 19 | import org.specs2.execute.{Failure, FailureException} 20 | 21 | /** To avoid bringin in hard dependencies on specs2 across the entire library 22 | * all integration should be kept in this module (and possibly broken out into 23 | * a separate library) 24 | */ 25 | object Specs2Integration { 26 | 27 | /** 28 | * Throw failures as FailureExceptions that specs2 will treat as failures instead 29 | * of errors. 30 | */ 31 | implicit object specs2FailureHandler extends MissingElementFailureHandler with TimeoutFailureHandler { 32 | 33 | def noSuchElement(msg: String): Unit = { 34 | throw new FailureException(Failure(msg)) 35 | } 36 | 37 | def fail(timeout: Timeout, msg: String): Unit = { 38 | throw new FailureException(Failure("Waited " + Time.humanText(timeout.inner) + " but " + msg)) 39 | } 40 | 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/scala/com/markatta/scalenium/Time.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Johan Andrén 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.markatta.scalenium 18 | 19 | import scala.concurrent.duration._ 20 | 21 | object Timeout { 22 | implicit val defaultTimeout = Timeout(5.seconds) 23 | } 24 | case class Timeout(inner: FiniteDuration) 25 | 26 | 27 | object Interval { 28 | implicit val defaultInterval = Interval(5.millis) 29 | } 30 | case class Interval(inner: FiniteDuration) 31 | 32 | private[scalenium] object Time { 33 | 34 | def humanText(d: FiniteDuration): String = 35 | if (d.toSeconds > 0) d.toSeconds + "s" 36 | else d.toMillis + "ms" 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/scala/com/markatta/scalenium/package.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Johan Andrén 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.markatta 18 | 19 | import scala.language.implicitConversions 20 | 21 | package object scalenium { 22 | 23 | implicit def seqOfElements2ElementSeq(elements: Seq[Element]): ElementSeq = new ElementSeq(elements) 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/test/scala/com/markatta/scalenium/DslSpec.scala: -------------------------------------------------------------------------------- 1 | package com.markatta.scalenium 2 | 3 | import org.specs2.mutable.Specification 4 | import org.openqa.selenium.firefox.FirefoxDriver 5 | import org.specs2.time.NoTimeConversions 6 | import scala.concurrent.duration._ 7 | 8 | class DslSpec extends Specification with NoTimeConversions { 9 | 10 | def mustCompile { 11 | // copy of the examples in the README, must compile 12 | // if anything is changed here, make sure you change the 13 | // readme as well 14 | 15 | // ex 1 16 | val b = new Browser(new FirefoxDriver()) 17 | 18 | // ex 2 19 | { 20 | b.find(".cssClass") // returns a Seq[Element] 21 | b.first(".cssClass") // returns an Option[Element] 22 | 23 | // aliases to find, for an additional style, see the JQuery syntax example below 24 | b.all(".cssClass") 25 | b.select(".cssClass") 26 | 27 | b.find(".someClass").isEmpty must beTrue 28 | b.find(".someClass") must haveSize(0) 29 | b.first(".someClass").isEmpty must beTrue 30 | b.find(".someClass").exists(_.id == "someId") must beTrue 31 | b.find(".someClass").size must equalTo(3) 32 | 33 | // Element contains the same ops as browser for searching so 34 | // these two gives the same result 35 | b.first(".someClass").get.first(".someOtherClass") 36 | b.first(".someClass > .someOtherClass") 37 | 38 | // Seq[Element] is implicitly converted to an ElementSeq by 39 | // import com.markatta.scalenium.seqOfElements2ElementSeq 40 | // allowing us to: 41 | 42 | b.find(".cssClass").hidden must beTrue // all elements matching .cssClass hidden 43 | b.find(".someClass > .someOtherClass").anySelected must beTrue 44 | b.find("li").texts must contain("Banana", "Pineapple") 45 | b.find("ul").find("li").size must equalTo(4) 46 | 47 | b.first("#it") must beSome 48 | b.first("#it").map(_.visible) must beSome(true) 49 | 50 | b.first("#doIt").foreach(_.click()) 51 | } 52 | 53 | // ex 3 54 | { 55 | implicit val brower = b 56 | import JqueryStyle._ 57 | $("#someId").visible must beTrue 58 | $("ul > li").exists(li => li.text == "Banana" && li.visible) must beTrue 59 | } 60 | 61 | // ex 4 62 | { 63 | b.write("newPassword").into("input[name='password']") 64 | b.write("happy").intoAll("input[type='text']") 65 | 66 | b.first("input[name='password']").get.write("newPassword") 67 | b.find("input[name='password']").write("newPassword") 68 | 69 | // fill entire form 70 | // identifying fields with name 71 | b.fillByName("user" -> "johndoe", "password" -> "newPasword") 72 | // id 73 | b.fillById("user" -> "johndoe", "password" -> "newPassword") 74 | // or css selector 75 | b.fill("input#user" -> "johndoe", "input[name='password']" -> "") 76 | } 77 | 78 | // ex 5 79 | { 80 | // to get specs2 failures instead of exceptions 81 | implicit val failureHandler = Specs2Integration.specs2FailureHandler 82 | 83 | b.waitFor(".someClass").toBecomeVisible() 84 | b.waitAtMost(5).secondsFor(".someClass").toBecomeVisible() 85 | 86 | b.waitFor(1 == 2).toBecomeTrue() 87 | b.waitAtMost(3000000).secondsFor(1 == 2).toBecomeTrue 88 | 89 | b.waitAtMost(5).secondsFor(b.find("button").disabled).toBecomeTrue 90 | } 91 | 92 | // ex 6 93 | { 94 | implicit val timeout = Timeout(3.seconds) 95 | implicit val interval = Interval(100.millis) 96 | 97 | b.waitFor(".someClass").toBecomeVisible() 98 | } 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /src/test/scala/com/markatta/scalenium/HttpTestServer.scala: -------------------------------------------------------------------------------- 1 | package com.markatta.scalenium 2 | 3 | import com.sun.net.httpserver._ 4 | import java.net.InetSocketAddress 5 | 6 | /** serve the same static content for any request for test purposes */ 7 | class HttpTestServer(port: Int, val contentPath: String, var staticContent: String) { 8 | 9 | val server = HttpServer.create(new InetSocketAddress(port), 0) 10 | 11 | server.createContext(contentPath, new HttpHandler(){ 12 | def handle(exchange: HttpExchange) { 13 | val bytes = staticContent.getBytes("UTF-8") 14 | exchange.sendResponseHeaders(200, bytes.length) 15 | val output = exchange.getResponseBody 16 | try { 17 | output.write(bytes) 18 | } finally { 19 | output.close() 20 | } 21 | } 22 | }) 23 | server.start() 24 | 25 | def stop() { 26 | server.stop(0) 27 | } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /src/test/scala/com/markatta/scalenium/ScaleniumSpec.scala: -------------------------------------------------------------------------------- 1 | package com.markatta.scalenium 2 | 3 | import org.specs2.mutable.Specification 4 | import com.markatta.scalenium._ 5 | import org.openqa.selenium.htmlunit.HtmlUnitDriver 6 | import org.specs2.specification.AfterExample 7 | 8 | class ScaleniumSpec extends Specification { 9 | 10 | var httpServer: Option[HttpTestServer] = None 11 | 12 | step{ 13 | httpServer = Some(new HttpTestServer(8080, "/test", someSimpleHtml)) 14 | } 15 | import Specs2Integration.specs2FailureHandler 16 | 17 | "markup search" should { 18 | 19 | "find elements from css selector" in { withBrowser { browser => 20 | browser.find("h1") should haveSize (1) 21 | browser.find("ul") should haveSize (1) 22 | browser.find("li") should haveSize (2) 23 | browser.find("li") should haveSize (2) 24 | browser.all("li") should haveSize (2) 25 | browser.select("li") should haveSize (2) 26 | browser.first("li") should beSome 27 | } 28 | } 29 | 30 | "handle missing elements gracefully" in { withBrowser { browser => 31 | browser.find("a") should beEmpty 32 | browser.first("a") should beNone 33 | } 34 | } 35 | } 36 | 37 | def withBrowser[T](testBlock: Browser => T): T = { 38 | val browser = new Browser(new HtmlUnitDriver()) 39 | browser.goTo("http://localhost:8080/test") 40 | testBlock(browser) 41 | } 42 | 43 | step { 44 | httpServer.foreach(_.stop()) 45 | } 46 | 47 | def someSimpleHtml = 48 | """ 49 | |the page 50 | | 51 | |

The header

52 | | 56 | | 57 | """.stripMargin 58 | 59 | } 60 | 61 | 62 | -------------------------------------------------------------------------------- /version.sbt: -------------------------------------------------------------------------------- 1 | version in ThisBuild := "1.0.4-SNAPSHOT" 2 | --------------------------------------------------------------------------------