├── .gitignore ├── src ├── main │ ├── resources │ │ └── log4j.properties │ ├── webapp │ │ ├── index.jsp │ │ ├── style │ │ │ └── style.css │ │ ├── WEB-INF │ │ │ ├── jsp │ │ │ │ ├── taglibs.jspf │ │ │ │ ├── helloPage.jsp │ │ │ │ ├── customer │ │ │ │ │ ├── customer-new.jsp │ │ │ │ │ ├── customer-edit.jsp │ │ │ │ │ └── customer-view.jsp │ │ │ │ └── home.jsp │ │ │ ├── tags │ │ │ │ └── head.tag │ │ │ ├── spring-context-web.xml │ │ │ ├── web.xml │ │ │ └── spring-context-data.xml │ │ └── static.htm │ └── scala │ │ └── com │ │ └── example │ │ └── scalawebapp │ │ ├── data │ │ ├── AbstractEntity.scala │ │ └── Customer.scala │ │ ├── controller │ │ ├── ControllerTools.scala │ │ ├── HelloWorldController.scala │ │ ├── HomePageController.scala │ │ └── CustomerController.scala │ │ └── repository │ │ └── CustomerRepository.scala └── test │ └── scala │ └── com │ └── example │ └── scalawebapp │ └── webtest │ ├── page │ ├── StaticFilePage.scala │ ├── ServerNamePage.scala │ ├── customer │ │ ├── ViewCustomerPage.scala │ │ ├── AddCustomerPage.scala │ │ └── EditCustomerPage.scala │ ├── HomePage.scala │ └── Page.scala │ ├── ServerNamePageWebTest.scala │ ├── StaticFileWebTest.scala │ ├── WebDriverAccess.scala │ └── CustomersWebTest.scala ├── LICENSE ├── README.md └── pom.xml /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | .classpath 3 | .idea 4 | out 5 | .project 6 | .settings 7 | .settings/* 8 | target 9 | *.iml 10 | -------------------------------------------------------------------------------- /src/main/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | log4j.rootCategory=INFO,Console 2 | log4j.logger.org.hibernate=WARN 3 | log4j.logger.org.springframework=WARN 4 | log4j.logger.org.springframework.web.servlet.DispatcherServlet=INFO 5 | 6 | log4j.appender.Console=org.apache.log4j.ConsoleAppender 7 | log4j.appender.Console.layout=org.apache.log4j.PatternLayout 8 | log4j.appender.Console.layout.ConversionPattern=%d [%t] %-5p %c - %m%n 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This code is in the public domain and may be used in any way you see fit, with the following conditions: 2 | 3 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 4 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 5 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 6 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 7 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 8 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | -------------------------------------------------------------------------------- /src/main/webapp/index.jsp: -------------------------------------------------------------------------------- 1 | <%-- 2 | * This code is in the public domain and may be used in any way you see fit, with the following conditions: 3 | * 4 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 5 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 6 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 7 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 8 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 9 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 10 | * THE SOFTWARE. 11 | --%> 12 | <%@page contentType="text/html;charset=utf-8"%> 13 | <%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> 14 | 15 | -------------------------------------------------------------------------------- /src/main/webapp/style/style.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This code is in the public domain and may be used in any way you see fit, with the following conditions: 3 | * 4 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 5 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 6 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 7 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 8 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 9 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 10 | * THE SOFTWARE. 11 | */ 12 | 13 | html { 14 | border: 0; 15 | } 16 | 17 | body { 18 | font-family: Helvetica, Arial, sans-serif; 19 | background-color: #eeeeee; 20 | } 21 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/jsp/taglibs.jspf: -------------------------------------------------------------------------------- 1 | <%-- 2 | * This code is in the public domain and may be used in any way you see fit, with the following conditions: 3 | * 4 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 5 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 6 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 7 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 8 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 9 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 10 | * THE SOFTWARE. 11 | --%> 12 | <%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> 13 | <%@taglib prefix="tags" tagdir="/WEB-INF/tags"%> 14 | <%@taglib prefix="form" uri="http://www.springframework.org/tags/form"%> 15 | -------------------------------------------------------------------------------- /src/test/scala/com/example/scalawebapp/webtest/page/StaticFilePage.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * This code is in the public domain and may be used in any way you see fit, with the following conditions: 3 | * 4 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 5 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 6 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 7 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 8 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 9 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 10 | * THE SOFTWARE. 11 | */ 12 | 13 | package com.example.scalawebapp.webtest.page 14 | 15 | import org.openqa.selenium.WebDriver 16 | 17 | class StaticFilePage(driver: WebDriver) extends Page[StaticFilePage]("static-file", driver) 18 | -------------------------------------------------------------------------------- /src/test/scala/com/example/scalawebapp/webtest/page/ServerNamePage.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * This code is in the public domain and may be used in any way you see fit, with the following conditions: 3 | * 4 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 5 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 6 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 7 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 8 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 9 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 10 | * THE SOFTWARE. 11 | */ 12 | 13 | package com.example.scalawebapp.webtest.page 14 | 15 | import org.openqa.selenium.WebDriver 16 | 17 | class ServerNamePage(driver: WebDriver) extends Page[ServerNamePage]("server-name", driver) { 18 | def serverName: String = textOf(id("serverName")) 19 | } -------------------------------------------------------------------------------- /src/main/scala/com/example/scalawebapp/data/AbstractEntity.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * This code is in the public domain and may be used in any way you see fit, with the following conditions: 3 | * 4 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 5 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 6 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 7 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 8 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 9 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 10 | * THE SOFTWARE. 11 | */ 12 | 13 | package com.example.scalawebapp.data 14 | 15 | import javax.persistence.{ MappedSuperclass, GeneratedValue, Id } 16 | 17 | @MappedSuperclass 18 | abstract class AbstractEntity { 19 | @Id 20 | @GeneratedValue 21 | var id: Long = 0 22 | 23 | def getId: Long = id 24 | } 25 | -------------------------------------------------------------------------------- /src/main/scala/com/example/scalawebapp/controller/ControllerTools.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * This code is in the public domain and may be used in any way you see fit, with the following conditions: 3 | * 4 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 5 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 6 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 7 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 8 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 9 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 10 | * THE SOFTWARE. 11 | */ 12 | 13 | package com.example.scalawebapp.controller 14 | 15 | import org.springframework.ui.ModelMap 16 | 17 | object ControllerTools { 18 | implicit def map2ModelMap(m: Map[String, Any]): ModelMap = { 19 | val mm = new ModelMap 20 | m.foreach((kv) => mm.addAttribute(kv._1, kv._2)) 21 | mm 22 | } 23 | } -------------------------------------------------------------------------------- /src/main/scala/com/example/scalawebapp/controller/HelloWorldController.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * This code is in the public domain and may be used in any way you see fit, with the following conditions: 3 | * 4 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 5 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 6 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 7 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 8 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 9 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 10 | * THE SOFTWARE. 11 | */ 12 | 13 | package com.example.scalawebapp.controller 14 | 15 | import org.springframework.stereotype.Controller 16 | import org.springframework.web.bind.annotation.RequestMapping 17 | 18 | @Controller 19 | class HelloWorldController { 20 | @RequestMapping(Array("/hello")) 21 | def showHello = "helloPage" 22 | } -------------------------------------------------------------------------------- /src/main/scala/com/example/scalawebapp/data/Customer.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * This code is in the public domain and may be used in any way you see fit, with the following conditions: 3 | * 4 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 5 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 6 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 7 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 8 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 9 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 10 | * THE SOFTWARE. 11 | */ 12 | 13 | package com.example.scalawebapp.data 14 | 15 | import beans.BeanProperty 16 | import javax.persistence.Entity 17 | 18 | @Entity 19 | class Customer extends AbstractEntity { 20 | @BeanProperty 21 | var name: String = null 22 | 23 | override def toString = "[Customer: id = " + id + ", name = " + name + "]" 24 | } 25 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/tags/head.tag: -------------------------------------------------------------------------------- 1 | <%-- 2 | * This code is in the public domain and may be used in any way you see fit, with the following conditions: 3 | * 4 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 5 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 6 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 7 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 8 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 9 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 10 | * THE SOFTWARE. 11 | --%> 12 | <%@tag body-content="tagdependent" %> 13 | <%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> 14 | <%@attribute name="title" required="true" rtexprvalue="true" type="java.lang.String" %> 15 | 16 | 17 | "> 18 | ${title} 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/main/webapp/static.htm: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | 16 | 17 | Static HTML 18 | 19 | 20 |

This is a static HTML file.

21 |

Home

22 | 23 | 24 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/jsp/helloPage.jsp: -------------------------------------------------------------------------------- 1 | <%-- 2 | * This code is in the public domain and may be used in any way you see fit, with the following conditions: 3 | * 4 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 5 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 6 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 7 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 8 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 9 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 10 | * THE SOFTWARE. 11 | --%> 12 | <%@page contentType="text/html;charset=utf-8"%> 13 | <%@include file="taglibs.jspf"%> 14 | 15 | 16 | 17 | 18 |

Hello from ${pageContext.request.serverName}

19 |

20 | " class="home-link">Home 21 |

22 | 23 | -------------------------------------------------------------------------------- /src/test/scala/com/example/scalawebapp/webtest/page/customer/ViewCustomerPage.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * This code is in the public domain and may be used in any way you see fit, with the following conditions: 3 | * 4 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 5 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 6 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 7 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 8 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 9 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 10 | * THE SOFTWARE. 11 | */ 12 | 13 | package com.example.scalawebapp.webtest.page.customer 14 | 15 | import org.openqa.selenium.WebDriver 16 | import com.example.scalawebapp.webtest.page.{HomePage, Page} 17 | 18 | class ViewCustomerPage(driver: WebDriver) extends Page[ViewCustomerPage]("customer-view", driver) { 19 | def deleteCustomer() = click(linkText("Delete the Customer")).expecting[HomePage] 20 | 21 | def edit() = click(linkText("Edit the Customer")).expecting[EditCustomerPage] 22 | } -------------------------------------------------------------------------------- /src/test/scala/com/example/scalawebapp/webtest/ServerNamePageWebTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * This code is in the public domain and may be used in any way you see fit, with the following conditions: 3 | * 4 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 5 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 6 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 7 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 8 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 9 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 10 | * THE SOFTWARE. 11 | */ 12 | 13 | package com.example.scalawebapp.webtest 14 | 15 | import com.example.scalawebapp.webtest.page._ 16 | import org.junit.Test 17 | import org.junit.Assert._ 18 | import org.hamcrest.CoreMatchers._ 19 | 20 | class ServerNamePageWebTest extends WebDriverAccess { 21 | @Test 22 | def shouldShowTheServerName { 23 | val homePage = HomePage.open() 24 | val serverNamePage = homePage.showServerName() 25 | assertThat(serverNamePage.serverName, is("localhost")) 26 | serverNamePage.home() 27 | } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /src/test/scala/com/example/scalawebapp/webtest/StaticFileWebTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * This code is in the public domain and may be used in any way you see fit, with the following conditions: 3 | * 4 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 5 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 6 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 7 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 8 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 9 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 10 | * THE SOFTWARE. 11 | */ 12 | 13 | package com.example.scalawebapp.webtest 14 | 15 | import com.example.scalawebapp.webtest.page._ 16 | import org.junit.Test 17 | import org.junit.Assert._ 18 | import org.hamcrest.CoreMatchers._ 19 | 20 | class StaticFileWebTest extends WebDriverAccess { 21 | @Test 22 | def shouldReturnTheContentsOfTheFile { 23 | val homePage = HomePage.open() 24 | val staticFilePage = homePage.showStaticFile() 25 | assertThat(staticFilePage.text, containsString("This is a static HTML file.")) 26 | staticFilePage.home() 27 | } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /src/test/scala/com/example/scalawebapp/webtest/page/customer/AddCustomerPage.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * This code is in the public domain and may be used in any way you see fit, with the following conditions: 3 | * 4 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 5 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 6 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 7 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 8 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 9 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 10 | * THE SOFTWARE. 11 | */ 12 | 13 | package com.example.scalawebapp.webtest.page.customer 14 | 15 | import org.openqa.selenium.WebDriver 16 | import com.example.scalawebapp.webtest.page.Page 17 | 18 | class AddCustomerPage(driver: WebDriver) extends Page[AddCustomerPage]("customer-new", driver) { 19 | def createCustomer(customerName: String) = { 20 | set(input("name"), customerName) 21 | submitExpecting[ViewCustomerPage] 22 | } 23 | 24 | def createCustomerExpectingFailure(customerName: String) = { 25 | set(input("name"), customerName) 26 | submitExpecting[AddCustomerPage] 27 | } 28 | } -------------------------------------------------------------------------------- /src/test/scala/com/example/scalawebapp/webtest/page/customer/EditCustomerPage.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * This code is in the public domain and may be used in any way you see fit, with the following conditions: 3 | * 4 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 5 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 6 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 7 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 8 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 9 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 10 | * THE SOFTWARE. 11 | */ 12 | 13 | package com.example.scalawebapp.webtest.page.customer 14 | 15 | import org.openqa.selenium.WebDriver 16 | import com.example.scalawebapp.webtest.page.Page 17 | 18 | class EditCustomerPage(driver: WebDriver) extends Page[EditCustomerPage]("customer-edit", driver) { 19 | def customerName = valueOf(input("name")) 20 | 21 | def updateCustomer(customerName: String) = { 22 | set(input("name"), customerName) 23 | submitExpecting[ViewCustomerPage] 24 | } 25 | 26 | def updateCustomerExpectingFailure(customerName: String) = { 27 | set(input("name"), customerName) 28 | submitExpecting[EditCustomerPage] 29 | } 30 | } -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/jsp/customer/customer-new.jsp: -------------------------------------------------------------------------------- 1 | <%-- 2 | * This code is in the public domain and may be used in any way you see fit, with the following conditions: 3 | * 4 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 5 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 6 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 7 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 8 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 9 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 10 | * THE SOFTWARE. 11 | --%> 12 | <%@page contentType="text/html;charset=utf-8"%> 13 | <%@include file="../taglibs.jspf"%> 14 | 15 | 16 | 17 | 18 |

New Customer

19 | 20 | Name 21 | 22 | 23 |
24 | 25 |
26 |

27 | ">Home 28 |

29 | 30 | 31 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/jsp/customer/customer-edit.jsp: -------------------------------------------------------------------------------- 1 | <%-- 2 | * This code is in the public domain and may be used in any way you see fit, with the following conditions: 3 | * 4 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 5 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 6 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 7 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 8 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 9 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 10 | * THE SOFTWARE. 11 | --%> 12 | <%@page contentType="text/html;charset=utf-8"%> 13 | <%@include file="../taglibs.jspf"%> 14 | 15 | 16 | 17 | 18 |

Edit Customer #:

19 | 20 | Name 21 | 22 | 23 |
24 | 25 |
26 |

27 | ">Home 28 |

29 | 30 | 31 | -------------------------------------------------------------------------------- /src/main/scala/com/example/scalawebapp/controller/HomePageController.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * This code is in the public domain and may be used in any way you see fit, with the following conditions: 3 | * 4 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 5 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 6 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 7 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 8 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 9 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 10 | * THE SOFTWARE. 11 | */ 12 | 13 | package com.example.scalawebapp.controller 14 | 15 | import org.springframework.stereotype.Controller 16 | import org.springframework.beans.factory.annotation.Autowired 17 | import org.hibernate.SessionFactory 18 | import org.springframework.web.bind.annotation.RequestMapping 19 | import org.springframework.web.bind.annotation.RequestMethod._ 20 | import org.springframework.web.servlet.ModelAndView 21 | import com.example.scalawebapp.repository.CustomerRepository 22 | 23 | @Controller 24 | class HomePageController { 25 | implicit def sessionFactory2Session(sf: SessionFactory) = sf.getCurrentSession 26 | 27 | @Autowired 28 | val customerRepository: CustomerRepository = null 29 | 30 | @RequestMapping(value = Array("/home"), method = Array(GET)) 31 | def loadCustomers() = 32 | new ModelAndView("home", "customers", customerRepository.getAll) 33 | } 34 | -------------------------------------------------------------------------------- /src/test/scala/com/example/scalawebapp/webtest/WebDriverAccess.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * This code is in the public domain and may be used in any way you see fit, with the following conditions: 3 | * 4 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 5 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 6 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 7 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 8 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 9 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 10 | * THE SOFTWARE. 11 | */ 12 | 13 | package com.example.scalawebapp.webtest 14 | 15 | import org.openqa.selenium.WebDriver 16 | import org.openqa.selenium.firefox.FirefoxDriver 17 | 18 | trait WebDriverAccess { 19 | implicit val driver = WebDriverAccess.defaultDriver 20 | } 21 | 22 | object WebDriverAccess { 23 | 24 | val closeBrowserAfterTests = true 25 | 26 | val defaultDriver: WebDriver = { 27 | print("Creating WebDriver... ") 28 | try { 29 | new FirefoxDriver() 30 | } finally { 31 | println("Done.") 32 | } 33 | } 34 | 35 | if (!isRunningInIde || closeBrowserAfterTests) { 36 | Runtime.getRuntime.addShutdownHook(new Thread(new Runnable { def run() {defaultDriver.quit()} } )) 37 | } 38 | 39 | private def isRunningInIde: Boolean = { 40 | Thread.currentThread.getStackTrace.find{_.getClassName.startsWith("com.intellij")} != None 41 | } 42 | } 43 | 44 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/jsp/customer/customer-view.jsp: -------------------------------------------------------------------------------- 1 | <%-- 2 | * This code is in the public domain and may be used in any way you see fit, with the following conditions: 3 | * 4 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 5 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 6 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 7 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 8 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 9 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 10 | * THE SOFTWARE. 11 | --%> 12 | <%@page contentType="text/html;charset=utf-8"%> 13 | <%@include file="../taglibs.jspf"%> 14 | 15 | 16 | 17 | 18 |

19 | Customer #: 20 |

21 |

Might I suggest enhancing the system to support more than just a customer's name?

22 |

23 | ">Edit the Customer 24 |

25 |

26 |

" method="POST"> 27 | 28 |
29 | Delete the Customer 30 |

31 |

32 | ">Home 33 |

34 | 35 | 36 | -------------------------------------------------------------------------------- /src/test/scala/com/example/scalawebapp/webtest/page/HomePage.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * This code is in the public domain and may be used in any way you see fit, with the following conditions: 3 | * 4 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 5 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 6 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 7 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 8 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 9 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 10 | * THE SOFTWARE. 11 | */ 12 | 13 | package com.example.scalawebapp.webtest.page 14 | 15 | import customer.{ViewCustomerPage, AddCustomerPage} 16 | import org.openqa.selenium.{By, WebDriver} 17 | import java.lang.String 18 | 19 | object HomePage { 20 | private val DEFAULT_PORT = 9090 21 | 22 | def open(server: String = "localhost", port: Int = DEFAULT_PORT)(implicit driver: WebDriver) = { 23 | driver.navigate.to("http://" + server + ":" + port + "/") 24 | new HomePage(driver) 25 | } 26 | } 27 | 28 | class HomePage(driver: WebDriver) extends Page[HomePage]("home", driver) { 29 | 30 | def showServerName() = click(linkText("Server Name")).expecting[ServerNamePage] 31 | def showStaticFile() = click(linkText("Static File")).expecting[StaticFilePage] 32 | def addCustomer() = click(linkText("Add a Customer")).expecting[AddCustomerPage] 33 | 34 | def viewCustomer(customerName: String) = click(By.partialLinkText(customerName)).expecting[ViewCustomerPage] 35 | 36 | def customerNames: Seq[String] = 37 | driver.findElement(By.id("customer-list")).findElements(By.tagName("a")).map(_.getText.split(": ")(1)) 38 | 39 | def deleteAllCustomers() = click(linkText("Delete All Customers")).expecting[HomePage] 40 | } -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/jsp/home.jsp: -------------------------------------------------------------------------------- 1 | <%-- 2 | * This code is in the public domain and may be used in any way you see fit, with the following conditions: 3 | * 4 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 5 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 6 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 7 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 8 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 9 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 10 | * THE SOFTWARE. 11 | --%> 12 | <%@page contentType="text/html;charset=utf-8"%> 13 | <%@include file="taglibs.jspf"%> 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |

22 | ">Server Name 23 |

24 |

25 | ">Static File 26 |

27 |

Customers

28 | 35 |

36 | ">Add a Customer 37 |

38 |

39 |

" method="POST"> 40 | 41 |
42 | Delete All Customers 43 |

44 | 45 | 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | :warning: **NOTE:** This repo hasn't been updated in 6 years and is now archived. :warning: 2 | 3 | Scala + Spring + Hibernate + Maven + Selenium WebDriver 4 | ======================================================= 5 | 6 | This project contains source code for kickstarting a webapp project using the latest versions of 7 | Scala, Spring, Hibernate and Maven, including Selenium WebDriver PageObject-based web tests. 8 | 9 | Versions currently used in the project are: 10 | 11 | * [Scala](http://www.scala-lang.org/): 2.11.2 12 | * [Spring](http://www.springsource.org/about): 4.0.6.RELEASE 13 | * [Hibernate](http://www.hibernate.org/): 4.3.6.Final 14 | 15 | The code also makes use of [HSQLDB](http://hsqldb.org/), but you'll probably want to replace that with the JDBC driver 16 | for whatever database you're using. 17 | 18 | 19 | Building and Running 20 | -------------------- 21 | 22 | Assuming you already have Maven installed, the webapp can be built by running: 23 | 24 | mvn clean install 25 | 26 | The webapp can be run inside Jetty using the Maven plugin: 27 | 28 | mvn jetty:run 29 | 30 | An Eclipse project will be generated with all the correct natures in it by running: 31 | 32 | mvn eclipse:eclipse 33 | 34 | 35 | Attribution 36 | ----------- 37 | 38 | This source code was originally created by Graham Lea for the blog http://grahamhackingscala.blogspot.com/ 39 | (though Graham now blogs at http://www.grahamlea.com/) 40 | 41 | The Eclipse plugin configuration was submitted by Trevor Lalish-Menagh (http://www.trevmex.com/) 42 | 43 | 44 | Conditions of Usage 45 | ------------------- 46 | 47 | This code is in the public domain and may be used in any way you see fit, with the following conditions: 48 | 49 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 50 | > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 51 | > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 52 | > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 53 | > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 54 | > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 55 | > THE SOFTWARE. 56 | -------------------------------------------------------------------------------- /src/main/scala/com/example/scalawebapp/repository/CustomerRepository.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * This code is in the public domain and may be used in any way you see fit, with the following conditions: 3 | * 4 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 5 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 6 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 7 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 8 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 9 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 10 | * THE SOFTWARE. 11 | */ 12 | 13 | package com.example.scalawebapp.repository 14 | 15 | import com.example.scalawebapp.data.Customer 16 | import org.springframework.stereotype.Repository 17 | import org.springframework.transaction.annotation.Transactional 18 | import org.springframework.beans.factory.annotation.Autowired 19 | import org.hibernate.SessionFactory 20 | 21 | trait CustomerRepository { 22 | def getAll: java.util.List[Customer] 23 | def save(customer: Customer): Long 24 | def update(customer: Customer) 25 | def get(customerId: Long): Customer 26 | def delete(customerId: Long) 27 | } 28 | 29 | @Repository 30 | class CustomerRepositoryImpl @Autowired() ( 31 | val sessionFactory: SessionFactory 32 | ) extends CustomerRepository { 33 | 34 | private def currentSession = sessionFactory.getCurrentSession 35 | 36 | @Transactional 37 | def save(customer: Customer): Long = Long.unbox(currentSession.save(customer).asInstanceOf[Object]) 38 | 39 | @Transactional 40 | def update(customer: Customer): Unit = currentSession.saveOrUpdate(customer) 41 | 42 | @Transactional 43 | def delete(customerId: Long): Unit = currentSession.delete(get(customerId)) 44 | 45 | @Transactional(readOnly = true) 46 | def get(customerId: Long): Customer = currentSession.get(classOf[Customer], Long.box(customerId)).asInstanceOf[Customer] 47 | 48 | @Transactional(readOnly = true) 49 | def getAll: java.util.List[Customer] = currentSession.createCriteria(classOf[Customer]).list().asInstanceOf[java.util.List[Customer]] 50 | } -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/spring-context-web.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 31 | 32 | 33 | 34 | 35 | 36 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/web.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 18 | 19 | 20 | contextConfigLocation 21 | /WEB-INF/spring-context-data.xml 22 | 23 | 24 | 25 | org.springframework.web.context.ContextLoaderListener 26 | 27 | 28 | 29 | dispatcher 30 | org.springframework.web.servlet.DispatcherServlet 31 | 32 | contextConfigLocation 33 | /WEB-INF/spring-context-web.xml 34 | 35 | 1 36 | 37 | 38 | 39 | dispatcher 40 | *.html 41 | 42 | 43 | 44 | 45 | methodFilter 46 | org.springframework.web.filter.HiddenHttpMethodFilter 47 | 48 | 49 | 50 | methodFilter 51 | *.html 52 | 53 | 54 | 55 | index.jsp 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/spring-context-data.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | com.example.scalawebapp.data.Customer 42 | 43 | 44 | 45 | 46 | org.hibernate.dialect.HSQLDialect 47 | false 48 | create 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /src/test/scala/com/example/scalawebapp/webtest/page/Page.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * This code is in the public domain and may be used in any way you see fit, with the following conditions: 3 | * 4 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 5 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 6 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 7 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 8 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 9 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 10 | * THE SOFTWARE. 11 | */ 12 | 13 | package com.example.scalawebapp.webtest.page 14 | 15 | import org.openqa.selenium.{By, WebDriver} 16 | import org.hamcrest.CoreMatchers._ 17 | import org.junit.Assert 18 | import java.lang.reflect.InvocationTargetException 19 | import collection.JavaConversions 20 | import org.openqa.selenium.By.ByName 21 | import org.openqa.selenium.support.ui.{ExpectedCondition, WebDriverWait} 22 | 23 | abstract class Page[T <: Page[T]](expectedBodyId: String, val driver: WebDriver) { 24 | 25 | self: T => 26 | 27 | waitUntil { !driver.findElements(By.tagName("body")).isEmpty } 28 | Assert.assertThat(driver.findElement(By.tagName("body")).getAttribute("id"), is(expectedBodyId)) 29 | 30 | final protected def id = By.id _ 31 | final protected def className = By.className _ 32 | final protected def linkText = By.linkText _ 33 | final protected def input = new ByName(_) 34 | final protected def xpath = By.xpath _ 35 | final protected def xpathFormat(text: String, arguments: Any*) = xpath(text.format(arguments)) 36 | final protected def spanWithId(id: String) = xpathFormat("//span[@id='%s']", id) 37 | 38 | final private val submitButton = xpath("//input[@type='submit']") 39 | 40 | lazy val text = driver.getPageSource.replaceAll("<[^>]+>", " ").replaceAll("[ \t]+", " ").replaceAll("\n( ?\n)+", "\n") 41 | 42 | protected def textOf(element: By) : String = driver.findElement(element).getText 43 | protected def valueOf(element: ByName) : String = driver.findElement(element).getAttribute("value") 44 | 45 | def submitButtonText : String = driver.findElement(submitButton).getAttribute("value") 46 | 47 | def home() = click(linkText("Home")).expecting[HomePage] 48 | 49 | protected def waitUntil(f: => Boolean): Unit = { 50 | new WebDriverWait(driver, 10, 50) until 51 | (new ExpectedCondition[Boolean] { def apply(input: WebDriver) = f }) 52 | } 53 | 54 | protected def set(inputBy: By, value: String) : T = { 55 | val inputElement = driver.findElement(inputBy) 56 | inputElement.clear() 57 | inputElement.sendKeys(value) 58 | this 59 | } 60 | 61 | protected def click(by: By) : T = { 62 | driver.findElement(by).click() 63 | this 64 | } 65 | 66 | protected def expecting[P <: Page[P]](implicit m: Manifest[P]): P = { 67 | try { 68 | m.runtimeClass.getConstructor(classOf[WebDriver]).newInstance(driver).asInstanceOf[P] 69 | } catch { 70 | case e: InvocationTargetException => throw e.getCause 71 | case e: Throwable => throw e 72 | } 73 | } 74 | 75 | protected implicit def javaList2Seq[A](list: java.util.List[A]): Seq[A] = JavaConversions.asScalaBuffer(list) 76 | 77 | def submitExpecting[P <: Page[P]](implicit m: Manifest[P]): P = click(submitButton).expecting[P] 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/test/scala/com/example/scalawebapp/webtest/CustomersWebTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * This code is in the public domain and may be used in any way you see fit, with the following conditions: 3 | * 4 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 5 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 6 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 7 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 8 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 9 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 10 | * THE SOFTWARE. 11 | */ 12 | 13 | package com.example.scalawebapp.webtest 14 | 15 | import com.example.scalawebapp.webtest.page._ 16 | import org.junit.Assert._ 17 | import org.hamcrest.CoreMatchers._ 18 | import org.junit.{After, Test} 19 | 20 | class CustomersWebTest extends WebDriverAccess { 21 | @After 22 | def deleteAllCustomers() { 23 | HomePage.open().deleteAllCustomers() 24 | } 25 | 26 | @Test 27 | def shouldBeAbleToBeAdded { 28 | val viewCustomerPage = createCustomerNamed("Adrian") 29 | assertThat(viewCustomerPage.text, containsString("Adrian")) 30 | } 31 | 32 | @Test 33 | def shouldNotBeAbleToBeAddedWithABlankName { 34 | val homePage = HomePage.open() 35 | var addCustomerPage = homePage.addCustomer() 36 | addCustomerPage = addCustomerPage.createCustomerExpectingFailure(customerName = "") 37 | assertThat(addCustomerPage.text, containsString("may not be empty")) 38 | } 39 | 40 | @Test 41 | def shouldBeAccessibleFromTheHomePageAfterBeingAdded { 42 | var viewCustomerPage = createCustomerNamed("Bruce") 43 | val homePage = viewCustomerPage.home() 44 | assertThat(homePage.customerNames.contains("Bruce"), is(true)) 45 | viewCustomerPage = homePage.viewCustomer("Bruce") 46 | assertThat(viewCustomerPage.text, containsString("Bruce")) 47 | } 48 | 49 | @Test 50 | def shouldBeAbleToBeEdited { 51 | val viewCustomerPage = createCustomerNamed("Carl") 52 | val editCustomerPage = viewCustomerPage.edit() 53 | assertThat(editCustomerPage.customerName, is("Carl")) 54 | editCustomerPage.updateCustomer(customerName = "Chris") 55 | assertThat(viewCustomerPage.text, containsString("Chris")) 56 | val homePage = viewCustomerPage.home() 57 | assertThat(homePage.customerNames.contains("Chris"), is(true)) 58 | assertThat(homePage.customerNames.contains("Carl"), is(false)) 59 | } 60 | 61 | @Test 62 | def shouldNotBeAbleToBeEditedToHaveABlankName { 63 | val viewCustomerPage = createCustomerNamed("David") 64 | var editCustomerPage = viewCustomerPage.edit() 65 | assertThat(editCustomerPage.customerName, is("David")) 66 | editCustomerPage = editCustomerPage.updateCustomerExpectingFailure(customerName = "") 67 | assertThat(editCustomerPage.text, containsString("may not be empty")) 68 | val homePage = editCustomerPage.home() 69 | assertThat(homePage.customerNames.contains("David"), is(true)) 70 | } 71 | 72 | @Test 73 | def shouldBeAbleToBeDeleted { 74 | val viewCustomerPage = createCustomerNamed("Elijah") 75 | val homePage = viewCustomerPage.deleteCustomer() 76 | assertThat(homePage.customerNames.contains("Elijah"), is(false)) 77 | } 78 | 79 | @Test 80 | def shouldBeAbleToBeDeletedAllAtOnce { 81 | createCustomerNamed("Frank") 82 | createCustomerNamed("George") 83 | val homePage = HomePage.open() 84 | assertThat(homePage.customerNames.contains("Frank"), is(true)) 85 | assertThat(homePage.customerNames.contains("George"), is(true)) 86 | homePage.deleteAllCustomers() 87 | assertThat(homePage.customerNames.contains("Frank"), is(false)) 88 | assertThat(homePage.customerNames.contains("George"), is(false)) 89 | } 90 | 91 | private def createCustomerNamed(name: String) = { 92 | val homePage = HomePage.open() 93 | val addCustomerPage = homePage.addCustomer() 94 | addCustomerPage.createCustomer(customerName = name) 95 | } 96 | 97 | } 98 | 99 | -------------------------------------------------------------------------------- /src/main/scala/com/example/scalawebapp/controller/CustomerController.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * This code is in the public domain and may be used in any way you see fit, with the following conditions: 3 | * 4 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 5 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 6 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 7 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 8 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 9 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 10 | * THE SOFTWARE. 11 | */ 12 | 13 | package com.example.scalawebapp.controller 14 | 15 | import org.springframework.stereotype.Controller 16 | import org.springframework.beans.factory.annotation.Autowired 17 | import org.springframework.web.bind.annotation.{ PathVariable, ModelAttribute, RequestMapping, RequestParam } 18 | import org.springframework.web.bind.annotation.RequestMethod._ 19 | import org.springframework.web.servlet.ModelAndView 20 | 21 | import com.example.scalawebapp.data.Customer 22 | import com.example.scalawebapp.repository.CustomerRepository 23 | import beans.BeanProperty 24 | import javax.validation.constraints.NotNull 25 | import org.hibernate.validator.constraints.NotEmpty 26 | import javax.validation.Valid 27 | import org.springframework.validation.BindingResult 28 | import collection.JavaConversions 29 | 30 | @Controller 31 | class CustomerController @Autowired() ( 32 | val customerRepository: CustomerRepository 33 | ) { 34 | 35 | import ControllerTools._ 36 | 37 | @RequestMapping(value = Array("/customers/new"), method = Array(GET)) 38 | def showNewCustomerForm() = new ModelAndView("customer/customer-new", "customerData", new CustomerPageData) 39 | 40 | @RequestMapping(value = Array("/customers/new"), method = Array(POST)) 41 | def createNewCustomer( 42 | @Valid @ModelAttribute("customerData") customerData: CustomerPageData, 43 | bindingResult: BindingResult): String = { 44 | if (bindingResult.hasErrors) { 45 | "customer/customer-new" 46 | } else { 47 | val newCustomer = new Customer 48 | customerData.copyTo(newCustomer) 49 | "redirect:/customers/" + customerRepository.save(newCustomer) + ".html" 50 | } 51 | } 52 | 53 | @RequestMapping(value = Array("/customers/{customerId}"), method = Array(GET)) 54 | def viewCustomer( 55 | @PathVariable customerId: Long, 56 | @RequestParam(required = false) edit: String) = { 57 | val customer: Customer = customerRepository.get(customerId) 58 | if (edit == null) { 59 | new ModelAndView("customer/customer-view", "customer", customer) 60 | } 61 | else { 62 | new ModelAndView("customer/customer-edit", Map("customer" -> customer, "customerData" -> CustomerPageData(customer))) 63 | } 64 | } 65 | 66 | @RequestMapping(value = Array("/customers/{customerId}"), method = Array(POST)) 67 | def editCustomer( 68 | @PathVariable customerId: Long, 69 | @Valid @ModelAttribute("customerData") customerData: CustomerPageData, 70 | bindingResult: BindingResult): ModelAndView = { 71 | val customer = customerRepository.get(customerId) 72 | if (bindingResult.hasErrors) { 73 | new ModelAndView("customer/customer-edit", "customer", customer) 74 | } else { 75 | customerData.copyTo(customer) 76 | customerRepository.update(customer) 77 | new ModelAndView("redirect:/customers/{customerId}.html") 78 | } 79 | } 80 | 81 | @RequestMapping(value = Array("/customers/{customerId}"), method = Array(DELETE)) 82 | def deleteCustomer(@PathVariable customerId: Long) = { 83 | customerRepository.delete(customerId) 84 | "redirect:/" 85 | } 86 | 87 | @RequestMapping(value = Array("/customers"), method = Array(DELETE)) 88 | def deleteAllCustomers() = { 89 | for (c: Customer <- JavaConversions.asScalaBuffer(customerRepository.getAll)) { 90 | customerRepository.delete(c.id) 91 | } 92 | "redirect:/" 93 | } 94 | } 95 | 96 | class CustomerPageData { 97 | @BeanProperty @NotNull @NotEmpty 98 | var name: String = null 99 | 100 | override def toString = "[CustomerPageData: name = " + name + "]" 101 | 102 | def copyTo(customer: Customer): Unit = { 103 | // TODO: Use Dozer to do this automatically 104 | customer.name = name 105 | } 106 | 107 | def copyFrom(customer: Customer): Unit = { 108 | // TODO: Use Dozer to do this automatically 109 | name = customer.name 110 | } 111 | } 112 | 113 | object CustomerPageData { 114 | def apply(c: Customer): CustomerPageData = { 115 | val data = new CustomerPageData 116 | data.copyFrom(c) 117 | data 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 4.0.0 5 | com.example.scalawebapp 6 | scala-spring-hibernate-maven-webapp 7 | 1.0-SNAPSHOT 8 | war 9 | 10 | 11 | 12 | Public Domain 13 | 14 | This code is in the public domain and may be used in any way you see fit, with the following conditions: 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 17 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 21 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | 24 | 25 | 26 | 27 | 28 | Graham Lea 29 | http://www.grahamlea.com 30 | graham.lea@belmonttechnology.com.au 31 | Australia/Sydney 32 | Belmont Technology Pty Ltd 33 | 34 | 35 | 36 | 37 | UTF-8 38 | 2.11.2 39 | 4.0.6.RELEASE 40 | 1.7.7 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | org.scala-lang 54 | scala-library 55 | ${scala.version} 56 | compile 57 | 58 | 59 | 60 | xerces 61 | xercesImpl 62 | 2.9.1 63 | runtime 64 | 65 | 66 | 67 | junit 68 | junit 69 | 4.11 70 | test 71 | 72 | 73 | 74 | org.hibernate 75 | hibernate-validator 76 | 5.1.2.Final 77 | compile 78 | 79 | 80 | 81 | org.springframework 82 | spring-context 83 | ${org.springframework.version} 84 | compile 85 | 86 | 87 | 88 | org.springframework 89 | spring-orm 90 | ${org.springframework.version} 91 | compile 92 | 93 | 94 | 95 | org.springframework 96 | spring-web 97 | ${org.springframework.version} 98 | compile 99 | 100 | 101 | 102 | org.springframework 103 | spring-webmvc 104 | ${org.springframework.version} 105 | compile 106 | 107 | 108 | 109 | javax.servlet 110 | javax.servlet-api 111 | 3.0.1 112 | provided 113 | 114 | 115 | 116 | javax.servlet.jsp 117 | javax.servlet.jsp-api 118 | 2.2.1 119 | provided 120 | 121 | 122 | 123 | javax.servlet.jsp.jstl 124 | javax.servlet.jsp.jstl-api 125 | 1.2.1 126 | provided 127 | 128 | 129 | 130 | commons-logging 131 | commons-logging 132 | 1.2 133 | runtime 134 | 135 | 136 | 137 | org.slf4j 138 | slf4j-api 139 | ${slf4j.version} 140 | runtime 141 | 142 | 143 | 144 | org.slf4j 145 | slf4j-log4j12 146 | ${slf4j.version} 147 | runtime 148 | 149 | 150 | 151 | org.hibernate 152 | hibernate-core 153 | 4.3.6.Final 154 | compile 155 | 156 | 157 | 158 | org.javassist 159 | javassist 160 | 3.18.2-GA 161 | runtime 162 | 163 | 164 | 165 | c3p0 166 | c3p0 167 | 0.9.1.2 168 | 169 | 170 | 171 | org.hsqldb 172 | hsqldb 173 | 2.3.2 174 | runtime 175 | 176 | 177 | 178 | org.seleniumhq.selenium 179 | selenium-java 180 | 2.42.2 181 | test 182 | 183 | 184 | 185 | com.google.code.findbugs 186 | jsr305 187 | 1.3.9 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | net.alchim31.maven 196 | scala-maven-plugin 197 | 3.2.0 198 | 199 | 200 | compile 201 | 202 | compile 203 | 204 | process-resources 205 | 206 | 207 | test-compile 208 | 209 | testCompile 210 | 211 | process-test-resources 212 | 213 | 214 | 215 | ${scala.version} 216 | 217 | 218 | 219 | org.apache.maven.plugins 220 | maven-compiler-plugin 221 | 3.1 222 | 223 | 1.7 224 | 1.7 225 | 226 | 227 | 228 | org.apache.maven.plugins 229 | maven-resources-plugin 230 | 2.6 231 | 232 | 233 | org.apache.maven.plugins 234 | maven-surefire-plugin 235 | 2.17 236 | 237 | true 238 | 239 | 240 | 241 | unit-tests 242 | test 243 | 244 | test 245 | 246 | 247 | false 248 | 249 | **/*Test.class 250 | **/*Spec.class 251 | 252 | 253 | **/webtest/**/*Test.class 254 | 255 | 256 | 257 | 258 | web-tests 259 | integration-test 260 | 261 | test 262 | 263 | 264 | false 265 | 266 | **/webtest/**/*Test.class 267 | 268 | 269 | 270 | 271 | 272 | 273 | org.eclipse.jetty 274 | jetty-maven-plugin 275 | 9.2.2.v20140723 276 | 277 | 278 | 279 | 9090 280 | 281 | stopIt 282 | 9191 283 | manual 284 | 285 | 286 | 287 | org.apache.jasper.compiler.disablejsr199 288 | true 289 | 290 | 291 | 292 | 293 | 294 | start-jetty-before-integration-tests 295 | pre-integration-test 296 | 297 | run 298 | 299 | 300 | 0 301 | true 302 | 303 | 304 | 305 | stop-jetty-after-integration-tests 306 | post-integration-test 307 | 308 | stop 309 | 310 | 311 | 312 | 313 | 314 | org.apache.maven.plugins 315 | maven-eclipse-plugin 316 | 2.9 317 | 318 | 319 | org.springframework.ide.eclipse.core.springbuilder 320 | org.eclipse.m2e.core.maven2Builder 321 | 322 | 323 | org.springframework.ide.eclipse.core.springnature 324 | org.eclipse.m2e.core.maven2Nature 325 | 326 | 327 | org.scala-ide.sdt.core.scalabuilder 328 | 329 | 330 | org.scala-ide.sdt.launching.SCALA_CONTAINER 331 | org.eclipse.jdt.launching.JRE_CONTAINER 332 | 333 | true 334 | true 335 | 336 | org.scala-lang:scala-library 337 | org.scala-lang:scala-compiler 338 | 339 | 340 | org.scala-ide.sdt.core.scalanature 341 | org.eclipse.jdt.core.javanature 342 | 343 | 344 | **/*.scala 345 | 346 | 2.0 347 | 348 | 349 | 350 | 351 | 352 | --------------------------------------------------------------------------------