Make the test browser's dependencies opt-in with `play-test-browser` by mkurz · Pull Request #14347 · playframework/playframework · GitHub
Skip to content

Make the test browser's dependencies opt-in with play-test-browser - #14347

Open
mkurz wants to merge 6 commits into
playframework:mainfrom
mkurz:play-test-browser
Open

mkurz wants to merge 6 commits into
playframework:mainfrom
mkurz:play-test-browser

Conversation

@mkurz

@mkurz mkurz commented Oct 10, 2026 •

Copy link
Copy Markdown
Member

Summary

play-test and play-specs2 brought Selenide, Selenium with all of its browser drivers and HtmlUnit to the tests of every application. Play's sbt plugin adds play-test to every project, so every application got them, although most tests don't use a browser. play-test resolves to 111 artifacts; 52 of them only come from the test browser.

Excluding them, as #13589 describes, broke tests without a browser too:

  • At runtime, play.api.test.Helpers and play.test.Helpers fail on first use with NoClassDefFoundError: org/openqa/selenium/htmlunit/HtmlUnitDriver, because the browser constants (HTMLUNIT, FIREFOX, …) are initialized eagerly.
  • At compile time, Scala can't compile any PlaySpecification without Selenium (on Scala 2.13 and 3), because it mixes in PlayRunners, whose browser runners mention WebDriver. On Scala 3, even code that only calls Helpers doesn't compile.

Changes:

  • Optional dependencies: play-test and play-specs2 declare selenide-core and htmlunit3-driver as optional dependencies, so applications no longer get them.
  • selenium-api stays: play-test still depends on Selenium's selenium-api (240 KB, plus the 3 KB jspecify), which contains WebDriver. Without it, Scala could not compile PlaySpecification, as described above. Its version must match the Selenium version Selenide brings, so Scala Steward doesn't update it on its own.
  • New play-test-browser artifact: it contains no code and brings play-test and the test browser's dependencies, in matching versions. The sbt plugin offers it as playTestBrowser, but doesn't add it automatically.
  • Scala form submission: submit is inherited from the existing Java browser base class. This keeps Selenide collection types out of TestBrowser's Scala signature, which otherwise broke compilation of non-browser tests. Positional, named, spliced and empty-field calls still work; the Scala sequence overload preserves the signature used by compiled callers. A method reference needs an expected function type to select that overload, as shown in the migration guide.
  • Browser constants:
    • In Scala, HTMLUNIT, FIREFOX, CHROME, EDGE and SAFARI are lazy and typed Class[? <: WebDriver]. The constructor of the specs2 WithBrowser that takes a driver class now takes Class[? <: WebDriver] as well, so that new WithBrowser(FIREFOX, app, port) keeps compiling.
    • In Java, they are looked up by name without initializing the class, and are null without the dependencies. Helpers.testBrowser(...) and Helpers.running(server, browserClass, ...) then fail with an error that explains how to add play-test-browser, before touching any browser class.

For applications:

  • Tests that don't use a browser no longer get 51 of these 52 artifacts (all but selenium-api), and Helpers, PlaySpecification, WithApplication and WithServer work without them.
  • Tests that use the test browser add one dependency line, libraryDependencies += playTestBrowser % Test, or org.playframework:play-test-browser_2.13/_3 with Gradle or Maven. Normal browser calls still work. In Scala, the browser constants are now typed Class[? <: WebDriver], so code that needs the exact driver class type, like val c: Class[FirefoxDriver] = FIREFOX, uses classOf[FirefoxDriver] instead. Method references to submit need an expected function type to select its Scala sequence overload; the migration guide includes an example. Without the dependency, they fail to compile or to run, because Selenide, the browser drivers or HtmlUnit are missing.
  • The testing guides, the 3.1 migration guide and the 3.1 highlights describe this.

Later: the browser classes and helpers could move into play-test-browser itself, so that play-test contains no Selenium code at all. This PR keeps them in play-test, so that browser tests keep their imports and APIs in the same release that switches them to Selenide (#14337).

Testing

  • Full CI on the previous head (eb938021b0): the Check workflow passed: all eight main-test lanes, eight documentation-test lanes, 48 scripted-test jobs, four Gradle-plugin jobs, formatting and binary compatibility checks. The matrix covers Java 17/25 and Scala 2.13, 3.3, 3.9 and 3.next; scripted tests cover sbt 1 and sbt 2. This run passed on its first attempt.
  • Direct regression: both the in-build without-browser spec and the scripted consumer construct "email" -> "user@example.com" in a PlaySpecification without the browser dependencies. The new example fails to compile on the original commit (7579549e58) with Scala 3.10.0-RC3 and the missing ElementsCollection/T error. With the fix, the without-browser suite passes on Scala 3.10.0-RC3, 3.3.8 and 2.13.18; root validateCode passes too.
  • CI follow-up: the first run exposed missing Selenide collection types while compiling the non-browser tests in Play-Ehcache, Play-JDBC, Play-AHC-WS and Play-Filters-Helpers on Scala 3.next (3.10.0-RC3). All four compile locally after moving submit to the Java base class. Browser form submissions and tests without browser dependencies pass on Scala 2.13.18, 3.3.8 and 3.10.0-RC3. The explicit method-reference replacement passes on 2.13.18 and 3.3.8. MiMa on both published Scala lines, root and documentation validateCode, API docs, and documentation link/snippet validation pass. A later CI run exposed an ambiguous Action overload in the new scripted fixture on Scala 3. Explicit request and result types fix that. The scripted consumer submits a form with named and spliced arguments against published jars, and now passes locally on Scala 2.13.18 and 3.3.8 with both sbt 1.12.15 and sbt 2.
  • Without the dependencies: the new, unpublished Play-Test-Without-Browser project excludes selenide-core and htmlunit3-driver, like an application without play-test-browser. On Scala 2.13 and 3.3.8:
    • specs2: PlaySpecification, Helpers, WithApplication and WithServer work, and the browsers and Selenide are not on the classpath.
    • JUnit: Helpers, running(app, ...) and running(testServer, ...) work, and Helpers.testBrowser() fails with the error that names playTestBrowser.
  • Consumer test: the new scripted test test-browser-dependencies builds two applications against the locally published Play (sbt's Ivy publication), as CI's scripted tests do, without excluding anything:
    • Without playTestBrowser: its specs2 and JUnit tests check that Selenide, HtmlUnit and the browser drivers are missing, so the test fails if play-test or play-specs2 bring them again. They also check that PlaySpecification, WithApplication, WithServer and Java Helpers work, that Helpers.HTMLUNIT is null, and that testBrowser() explains the missing dependency.
    • With playTestBrowser: specs2 WithBrowser (with the HTMLUNIT constant) and Java running(testServer, HTMLUNIT, ...) open a page of the application with HtmlUnit.
    • The extended form-submission fixture passes locally with both sbt 1.12.15 and sbt 2 on Scala 2.13.18 and 3.3.8. The full CI matrix passes on the latest head, including the new tuple regression in the consumer fixture.
  • WithBrowser with a driver class: the new WithBrowserSpec in Play-Integration-Test passes HTMLUNIT to WithBrowser, without and with an explicit driver type. Both failed to compile with the previous constructor, and pass on Scala 2.13 and 3.3.8.
  • Existing tests: Play-Integration-Test now depends on Play-Test-Browser for its WithBrowser examples (also those of Reset the port of specs2 WithServer and WithBrowser also when an example fails #14338), like an application with playTestBrowser; they pass on Scala 2.13 and 3.3.8. The Play-Test and Play-Specs2 tests, MiMa, validateCode, and the documentation's browser examples (Java and Scala, now with Play-Test-Browser) and validation pass. HelpersTest checks that the Java constants, now looked up by name, are the driver classes, and that a null driver class gets the explaining error.
  • Gradle consumer test: the Gradle plugin’s functional tests build the same two applications from Play’s Maven POMs, permanently checking that the browser dependencies remain optional. The focused test passes on Scala 2.13 and 3 with Gradle 7.6.2, 8.0.2 and 9.4.1. All four consumer XML suites must report at least one test and zero skipped tests, failures and errors. The earlier control removed the selenide-core optional flag from the local play-test POM and failed as expected; the POM was restored. Both specs use JUnitRunner for Gradle; the earlier sbt consumer checks still passed with each spec running once.
  • Published Maven artifacts: I checked these once by hand: I published Play to a local Maven repository under a test version, as Gradle and Maven users get it.
    • The POMs of play-test and play-specs2 mark selenide-core and htmlunit3-driver as optional, and play-test-browser depends on them normally.
    • Two separate sbt projects resolved Play from there, on Scala 2.13.18 and 3.3.8:
      • Without play-test-browser: of the Selenium family, only selenium-api is on the test classpath. A PlaySpecification using Helpers, WithApplication and WithServer, and a Java test using Helpers and testBrowser()'s error, pass.
      • With play-test-browser: WithBrowser and runningWithPort(..., HTMLUNIT) in specs2, and running(testServer(), HTMLUNIT, ...) in Java, pass with HtmlUnit.
    • play-test now resolves to 60 artifacts instead of 111 (51 fewer); with play-test-browser, it's 112.

AI assistance

Claude (Anthropic) was used to analyse the issue, implement this change, write the tests and draft this pull request. OpenAI Codex reviewed the approach and implemented the Scala signature fix and its compatibility notes after CI exposed the issue. I reviewed the result before submitting.

mkurz and others added 5 commits October 10, 2026 14:02
play-test, which Play's sbt plugin adds to every project, and
play-specs2 brought Selenide, Selenium with all of its browser drivers
and HtmlUnit to the tests of every application, about 50 artifacts,
although most tests don't use a browser. Excluding them broke tests
without a browser too: play.api.test.Helpers and play.test.Helpers
failed with a NoClassDefFoundError for HtmlUnitDriver, and Scala could
not compile any PlaySpecification (on Scala 3 not even code using
Helpers) without Selenium's WebDriver.

- play-test and play-specs2 now declare selenide-core and
  htmlunit3-driver as optional dependencies. play-test only depends on
  the small selenium-api, which contains WebDriver.
- The new play-test-browser artifact (sbt: playTestBrowser) contains no
  code and brings these dependencies in matching versions.
- In Scala, the browser constants are lazy and typed
  Class[? <: WebDriver], and so is the driver class that the specs2
  WithBrowser constructor takes. In Java, they are looked up by name and
  are null without the dependencies; Helpers then explains how to add
  them.
- The new Play-Test-Without-Browser project tests Helpers,
  PlaySpecification, WithApplication and WithServer without the
  optional dependencies. The new scripted test
  test-browser-dependencies checks the published artifacts: an
  application gets the browser dependencies only with playTestBrowser.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Move form submission into the existing Java browser base class so Scala can load TestBrowser signatures without completing Selenide collection types. Keep the Seq signature for compiled Scala callers and preserve named arguments with Java parameter metadata.

Exercise named, spliced and empty form submissions, and extend the published-artifact consumer fixture. Verify the four failing Scala 3.next module compilations and the browser and no-browser suites on Scala 2.13, 3.3 and 3.next.
While TestBrowser.submit was defined in Scala, Scala 3.10 failed to
compile `"a" -> "b"` in a PlaySpecification without Selenide ("Bad
symbolic reference" to ElementsCollection), also against artifacts
built with Scala 3.3. The without-browser project and the scripted
consumer test now check it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mkurz

mkurz commented Oct 11, 2026

Copy link
Copy Markdown
Member Author

The scripted test resolves Play from the local Ivy repository, so it
doesn't check the Maven POMs that Gradle and Maven users get. The
Gradle plugin's functional tests now build the same two applications
from Play's POMs: without play-test-browser, Selenide, HtmlUnit and
the browser drivers must be missing.

Gradle runs the specs through JUnit, so they get JUnitRunner.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Isolate Selenium from play.api.test.Helpers

1 participant