diff --git a/documentation/manual/working/javaGuide/main/tests/JavaFunctionalTest.md b/documentation/manual/working/javaGuide/main/tests/JavaFunctionalTest.md index 2cc095367fe..a6d63a13eb9 100644 --- a/documentation/manual/working/javaGuide/main/tests/JavaFunctionalTest.md +++ b/documentation/manual/working/javaGuide/main/tests/JavaFunctionalTest.md @@ -68,7 +68,7 @@ Just as there exists a `WithApplication` class, there is also a [`WithServer`](a ## Testing with a browser -If you want to test your application from with a Web browser, you can use [Selenium WebDriver](https://github.com/seleniumhq/selenium). Play will start the WebDriver for you, and wrap it in the convenient API provided by [FluentLenium](https://github.com/FluentLenium/FluentLenium). +If you want to test your application from with a Web browser, you can use [Selenium WebDriver](https://github.com/seleniumhq/selenium). Play will start the WebDriver for you, and wrap it in the convenient API provided by [Selenide](https://github.com/selenide/selenide). @[test-browser](code/javaguide/tests/FunctionalTest.java) diff --git a/documentation/manual/working/scalaGuide/main/tests/ScalaFunctionalTestingWithSpecs2.md b/documentation/manual/working/scalaGuide/main/tests/ScalaFunctionalTestingWithSpecs2.md index e32ccf08f49..5759ab84967 100644 --- a/documentation/manual/working/scalaGuide/main/tests/ScalaFunctionalTestingWithSpecs2.md +++ b/documentation/manual/working/scalaGuide/main/tests/ScalaFunctionalTestingWithSpecs2.md @@ -36,7 +36,7 @@ An application can also be passed to the test server, which is useful for settin ## WithBrowser -If you want to test your application using a browser, you can use [Selenium WebDriver](https://github.com/seleniumhq/selenium). Play will start the WebDriver for you, and wrap it in the convenient API provided by [FluentLenium](https://github.com/FluentLenium/FluentLenium) using [`WithBrowser`](api/scala/play/api/test/WithBrowser.html). Like [`WithServer`](api/scala/play/api/test/WithServer.html), you can change the port, [`Application`](api/scala/play/api/Application.html), and you can also select the web browser to use: +If you want to test your application using a browser, you can use [Selenium WebDriver](https://github.com/seleniumhq/selenium). Play will start the WebDriver for you, and wrap it in the convenient API provided by [Selenide](https://github.com/selenide/selenide) using [`WithBrowser`](api/scala/play/api/test/WithBrowser.html). Like [`WithServer`](api/scala/play/api/test/WithServer.html), you can change the port, [`Application`](api/scala/play/api/Application.html), and you can also select the web browser to use: @[scalafunctionaltest-testwithbrowser](code/specs2/ScalaFunctionalTestSpec.scala) diff --git a/project/BuildSettings.scala b/project/BuildSettings.scala index 9e2400ec247..490d9b90274 100644 --- a/project/BuildSettings.scala +++ b/project/BuildSettings.scala @@ -459,6 +459,11 @@ object BuildSettings { ProblemFilters.exclude[MissingClassProblem]("play.utils.ReadingMap$"), // Remove unused, package-private method ProblemFilters.exclude[DirectMissingMethodProblem]("play.api.libs.ws.ahc.AhcWSClient.loggerFactory"), + // Replace FluentLenium with Selenide + ProblemFilters.exclude[MissingTypesProblem]("play.api.test.TestBrowser"), + ProblemFilters.exclude[DirectMissingMethodProblem]("play.api.test.TestBrowser.submit"), + ProblemFilters.exclude[MissingTypesProblem]("play.test.TestBrowser"), + ProblemFilters.exclude[DirectMissingMethodProblem]("play.api.test.WithBrowser.browser"), ), (Compile / unmanagedSourceDirectories) += { val suffix = CrossVersion.partialVersion(scalaVersion.value) match { diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 0439d6b9b35..6f50e25078c 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -259,31 +259,9 @@ object Dependencies { "org.apache.pekko" %% "pekko-cluster-sharding-typed" % pekkoVersion ) - val fluentleniumVersion = "6.0.0" - // This is the selenium version compatible with the FluentLenium version declared above. - // See https://repo1.maven.org/maven2/io/fluentlenium/fluentlenium-parent/6.0.0/fluentlenium-parent-6.0.0.pom - val seleniumVersion = "4.14.1" - val htmlunitVersion = "4.13.0" - val testDependencies = Seq(junit, junitInterface, guava, logback) ++ Seq( - ("io.fluentlenium" % "fluentlenium-core" % fluentleniumVersion) - .exclude("org.jboss.netty", "netty") - .excludeAll(ExclusionRule("commons-beanutils", "commons-beanutils")) // comes with CVE-2025-48734 - .excludeAll(ExclusionRule("commons-io", "commons-io")), // comes with outdated commons-io - // htmlunit-driver uses an open range to selenium dependencies. This is slightly - // slowing down the build. So the open range deps were removed and we can re-add - // them using a specific version. Using an open range is also not good for the - // local cache. - ("org.seleniumhq.selenium" % "htmlunit-driver" % htmlunitVersion).excludeAll( - ExclusionRule("org.seleniumhq.selenium", "selenium-api"), - ExclusionRule("org.seleniumhq.selenium", "selenium-support"), - ExclusionRule("commons-io", "commons-io") // comes with outdated commons-io - ), - "commons-beanutils" % "commons-beanutils" % "1.11.0", // explicitly bump for fluentlenium and htmlunit to fix CVE-2025-48734 - "commons-io" % "commons-io" % "2.20.0", // explicitly bump commons-io to newer version for fluentlenium and htmlunit - "org.seleniumhq.selenium" % "selenium-api" % seleniumVersion, - "org.seleniumhq.selenium" % "selenium-support" % seleniumVersion, - "org.seleniumhq.selenium" % "selenium-firefox-driver" % seleniumVersion + "com.codeborne" % "selenide" % "7.11.1", + "org.seleniumhq.selenium" % "htmlunit3-driver" % "4.36.0", ) ++ guiceDeps ++ specs2Deps.map(_ % Test) :+ mockitoAll % Test val playCacheDeps = specs2Deps.map(_ % Test) :+ logback % Test diff --git a/testkit/play-specs2/src/main/scala/play/api/test/Specs.scala b/testkit/play-specs2/src/main/scala/play/api/test/Specs.scala index d9f5bfd4653..a98afa38ac3 100644 --- a/testkit/play-specs2/src/main/scala/play/api/test/Specs.scala +++ b/testkit/play-specs2/src/main/scala/play/api/test/Specs.scala @@ -194,20 +194,20 @@ abstract class WithBrowser[WEBDRIVER <: WebDriver]( implicit def implicitApp: Application = app implicit def implicitPort: Port = port - - lazy val browser: TestBrowser = TestBrowser(webDriver, Some("http://localhost:" + port)) + var browser: TestBrowser = null // TODO not var! just temporary now. either def or private[test] setBaseUrl override def wrap[T: AsResult](t: => T): Result = { try { val currentPort = port val result = Helpers.runningWithPort(TestServer(port, app)) { assignedPort => port = assignedPort // if port was 0, the OS assigns a random port + browser = new TestBrowser(webDriver, Some("http://localhost:" + assignedPort)) AsResult.effectively(t) } port = currentPort result } finally { - browser.quit() + Option(browser).foreach(_.quit()) } } } diff --git a/testkit/play-test/src/main/java/play/test/TestBrowser.java b/testkit/play-test/src/main/java/play/test/TestBrowser.java index 681ce3795cc..69637420433 100644 --- a/testkit/play-test/src/main/java/play/test/TestBrowser.java +++ b/testkit/play-test/src/main/java/play/test/TestBrowser.java @@ -4,21 +4,25 @@ package play.test; -import io.fluentlenium.adapter.FluentAdapter; +import com.codeborne.selenide.SelenideConfig; +import com.codeborne.selenide.SelenideDriver; +import com.codeborne.selenide.SelenideElement; import java.time.Duration; import java.util.function.Function; import org.openqa.selenium.WebDriver; import org.openqa.selenium.support.ui.FluentWait; /** - * A test browser (Using Selenium WebDriver) with the FluentLenium API - * (https://github.com/Fluentlenium/FluentLenium). + * A test browser (Using Selenium WebDriver) with the Selenide API + * (https://github.com/selenide/selenide). */ -public class TestBrowser extends FluentAdapter { +public class TestBrowser { + + private SelenideDriver driver; /** - * A test browser (Using Selenium WebDriver) with the FluentLenium API - * (https://github.com/Fluentlenium/FluentLenium). + * A test browser (Using Selenium WebDriver) with the Selenide API + * (https://github.com/selenide/selenide). * * @param webDriver The WebDriver instance to use. * @param baseUrl The base url to use for relative requests. @@ -29,15 +33,45 @@ public TestBrowser(Class webDriver, String baseUrl) throws } /** - * A test browser (Using Selenium WebDriver) with the FluentLenium API - * (https://github.com/Fluentlenium/FluentLenium). + * A test browser (Using Selenium WebDriver) with the Selenide API + * (https://github.com/selenide/selenide). * * @param webDriver The WebDriver instance to use. * @param baseUrl The base url to use for relative requests. */ public TestBrowser(WebDriver webDriver, String baseUrl) { - super.initFluent(webDriver); - super.getConfiguration().setBaseUrl(baseUrl); + SelenideConfig config = new SelenideConfig(); + config.baseUrl(baseUrl); + driver = new SelenideDriver(config, webDriver, null); + } + + public void open(String relativeOrAbsoluteUrl) { + driver.open(relativeOrAbsoluteUrl); + } + + public void goTo(String relativeOrAbsoluteUrl) { + open(relativeOrAbsoluteUrl); + } + + public String source() { + return driver.source(); + } + + public String pageSource() { + return source(); + } + + public SelenideElement el(String cssSelector) { + return driver.find(cssSelector); + } + + public SelenideElement $(String cssSelector) { + return el(cssSelector); + } + + public String url() { + // return the relative url + return driver.url().substring(driver.config().baseUrl().length() + 1); } /** @@ -46,7 +80,7 @@ public TestBrowser(WebDriver webDriver, String baseUrl) { * @return the webdriver contained in a fluent wait. */ public FluentWait fluentWait() { - return new FluentWait<>(super.getDriver()); + return new FluentWait<>(driver.getWebDriver()); } /** @@ -54,9 +88,6 @@ public FluentWait fluentWait() { * occurs: the function returns neither null nor false, the function throws an unignored * exception, the timeout expires * - *

Useful in situations where FluentAdapter#await is too specific (for example to check against - * page source) - * * @param the return type * @param wait generic {@code FluentWait} instance * @param f function to execute @@ -76,9 +107,6 @@ public T waitUntil(FluentWait wait, Function f) { *

  • the default timeout expires * * - * useful in situations where FluentAdapter#await is too specific (for example to check against - * page source or title) - * * @param f function to execute * @param the return type * @return the return value. @@ -95,14 +123,16 @@ public T waitUntil(Function f) { * @return the web driver options. */ public WebDriver.Options manage() { - return super.getDriver().manage(); + return driver.getWebDriver().manage(); } /** Quits and releases the {@link WebDriver} */ void quit() { - if (getDriver() != null) { - getDriver().quit(); + // TODO siehe dprecation comment in WebDriverRunner.closeWebDriver + final WebDriver webDriver = driver.getWebDriver(); + if (webDriver != null) { + webDriver.quit(); } - releaseFluent(); + // releaseFluent(); } } diff --git a/testkit/play-test/src/main/scala/play/api/test/Selenium.scala b/testkit/play-test/src/main/scala/play/api/test/Selenium.scala index 3981357fbbc..017a5a0c239 100644 --- a/testkit/play-test/src/main/scala/play/api/test/Selenium.scala +++ b/testkit/play-test/src/main/scala/play/api/test/Selenium.scala @@ -8,39 +8,43 @@ import java.util.concurrent.TimeUnit import scala.jdk.FunctionConverters._ -import io.fluentlenium.adapter.FluentAdapter -import io.fluentlenium.core.domain.FluentList -import io.fluentlenium.core.domain.FluentWebElement +import com.codeborne.selenide.SelenideConfig +import com.codeborne.selenide.SelenideDriver +import com.codeborne.selenide.SelenideElement import org.openqa.selenium._ import org.openqa.selenium.firefox._ import org.openqa.selenium.htmlunit._ import org.openqa.selenium.support.ui.FluentWait /** - * A test browser (Using Selenium WebDriver) with the FluentLenium API (https://github.com/Fluentlenium/FluentLenium). + * A test browser (Using Selenium WebDriver) with the Selenide API (https://github.com/selenide/selenide). * * @param webDriver The WebDriver instance to use. */ -case class TestBrowser(webDriver: WebDriver, baseUrl: Option[String]) extends FluentAdapter() { - super.initFluent(webDriver) - baseUrl.foreach(baseUrl => super.getConfiguration.setBaseUrl(baseUrl)) +case class TestBrowser(webDriver: WebDriver, baseUrl: Option[String]) { + private val config = new SelenideConfig() + baseUrl.foreach(baseUrl => config.baseUrl(baseUrl)) + private val driver = new SelenideDriver(config, webDriver, null) - /** - * Submits a form with the given field values - * - * @example {{{ - * submit("#login", fields = - * "email" -> email, - * "password" -> password - * ) - * }}} - */ - def submit(selector: String, fields: (String, String)*): FluentList[FluentWebElement] = { - fields.foreach { - case (fieldName, fieldValue) => - $(s"$selector *[name=$fieldName]").fill.`with`(fieldValue) - } - $(selector).submit() + def open(relativeOrAbsoluteUrl: String): Unit = { + driver.open(relativeOrAbsoluteUrl) + } + + def goTo(relativeOrAbsoluteUrl: String): Unit = { + open(relativeOrAbsoluteUrl) + } + + def source: String = driver.source + + def pageSource: String = source + + def el(cssSelector: String): SelenideElement = driver.find(cssSelector) + + def $(cssSelector: String): SelenideElement = el(cssSelector) + + def url: String = { + // return the relative url + driver.url().substring(driver.config().baseUrl().length + 1) } /** @@ -88,11 +92,11 @@ case class TestBrowser(webDriver: WebDriver, baseUrl: Option[String]) extends Fl * retrieves the underlying option interface that can be used * to set cookies, manage timeouts among other things */ - def manage: WebDriver.Options = super.getDriver.manage + def manage: WebDriver.Options = driver.getWebDriver().manage def quit(): Unit = { - Option(super.getDriver).foreach(_.quit()) - releaseFluent() + Option(driver.getWebDriver()).foreach(_.quit()) + // releaseFluent() } }