selenider is compatible with unit testing frameworks like
testthat
and shinytest2
. In this vignette, we
will explore how to write unit tests with selenider, and we will briefly
describe how to automate your tests using Github Actions.
Tests contained within testthat::test_that()
are
self-contained, having no impact on other tests. selenider is no
exception: selenider sessions, when created inside a
testthat::test_that()
block, will be closed automatically
when the test finishes running.
Remember, as always, to use the .env
argument when
wrapping selenider_session()
in another function.
elem_expect()
also has additional features inside
testhat::test_that()
. When it succeeds, it will call
testthat::succeed()
, and when it fails, it will use
testthat::fail()
instead of throwing an error. This allows
tests to continue running even if elem_expect()
fails.
test_that("My test", {
# session will be opened here...
open_url("https://www.r-project.org/")
s(".random-class") |>
elem_expect(is_present)
}) # and closed here!
#> -- Failure: My test ------------------------------------------------------------
#> Condition failed after waiting for 4 seconds:
#> `is_present`
#> i `x` is not present.
#>
#> Where `x` is:
#> A selenider element selecting:
#> The first element with css selector ".random-class".
#> Error:
#> ! Test failed
Since shinytest2 uses chromote as a backend, it can be used with
selenider. selenider can be used to add more robust UI testing to
shinytest2, replacing unreliable uses of
AppDriver$expect_screenshot()
.
shinytest2 does have a few UI expectations
(AppDriver$expect_text()
,
AppDriver$expect_html()
and
AppDriver$expect_js()
), but these do not include the same
laziness and implicit waiting that selenider provides, making them a bit
less reliable.
Let’s create a simple shiny app, consisting of a
shiny::actionButton()
and
shiny:: conditionalPanel()
. The panel is shown if the
button has been clicked an odd number of times, and hidden
otherwise.
We would like to test that the server-side processing of the button input is done correctly, which we can do using shinytest2. However, we would also like to check that the panel is visible at the correct times, which we cannot do with shinytest2, and so we will use selenider instead.
shiny_app <- shinyApp(
ui = fluidPage(
actionButton("button", label = "Click me!"),
conditionalPanel(
condition = "(input.button % 2) == 1",
p("Button has been clicked an odd number of times.")
) |>
tagAppendAttributes(id = "condpanel")
),
server = function(input, output) {
even <- reactive((input$button %% 2) == 0)
exportTestValues(even = {
even()
})
}
)
To start a selenider session using an existing
shinytest2::AppDriver
object, supply it to the
driver
argument of selenider_session()
:
session <- selenider_session(driver = <AppDriver>)
test_that("App works", {
app <- AppDriver$new(shiny_app)
session <- selenider_session(driver = app)
s("#condpanel") |>
elem_expect(is_invisible)
app$click("button")
app$expect_values(export = "even")
s("#condpanel") |>
elem_expect(is_visible)
app$click("button")
app$expect_values(export = "even")
s("#condpanel") |>
elem_expect(is_invisible)
})
#> Can't compare snapshot to reference when testing interactively.
#> i Run `devtools::test()` or `testthat::test_file()` to see changes.
#> New path: /tmp/RtmpRTtTRm/st2-8ebae427132/001_.png
#> Can't compare snapshot to reference when testing interactively.
#> i Run `devtools::test()` or `testthat::test_file()` to see changes.
#> New path: /tmp/RtmpRTtTRm/st2-8ebae427132/001.json
#> Can't compare snapshot to reference when testing interactively.
#> i Run `devtools::test()` or `testthat::test_file()` to see changes.
#> New path: /tmp/RtmpRTtTRm/st2-8ebae427132/002_.png
#> Can't compare snapshot to reference when testing interactively.
#> i Run `devtools::test()` or `testthat::test_file()` to see changes.
#> New path: /tmp/RtmpRTtTRm/st2-8ebae427132/002.json
#> Test passed
Note the difference in styles: while in selenider you must specify tests explicitly, shinytest2 uses a snapshot-based approach (specifying the value that you want to test and omitting the value that you expect it to be). There are advantages and disadvantages to this approach: the tests are generally easier to create and update, but a little harder to debug.
If you want to use a snapshot-based style, you can do it manually, e.g.:
However, note that the tests will no longer wait a certain period of time for the value to be correct, since the test is unaware of what the correct value is.
The complexity of using selenider with Github Actions depends on the backend that you use.
If you would like to use chromote as your backend, you shouldn’t need to make any special additions to your workflow files, and can safely use something like r-lib’s R CMD CHECK action. This is because chromote only requires chrome to be installed, which is already the case on Github’s machines.
If you want to use selenium with Github Actions, it is recommended to make use of docker. See https://github.com/SeleniumHQ/docker-selenium for more information. For example, the following lines in a Github Actions yaml file will start a selenium server (version 4.15.0), supporting Firefox, on port 4444. We recommend using the “shm-size” argument to make sure you don’t run out of memory.
services:
selenium:
image: selenium/standalone-firefox:4.15.0-20231108
ports:
- 4444:4444
options: >-
--shm-size="2g"
This will download Firefox and start a Selenium server on port 4444.
Automating a browser with Selenium consists of two parts: the server and
the client. By default, selenider_session()
tries to setup
both, but we can stop this from happening by using the
options
argument.
session <- selenider_session(
"selenium",
browser = "firefox",
options = selenium_options(
server_options = NULL, # Stop selenider from creating a server
client_options = selenium_client_options(port = 4444L) # Use the port of the server
)
)
The session can then by used as usual. selenider will no longer be able to close the selenium server, but this should be done automatically in the Github Action.
For more information, see how we setup our Github Actions workflow for selenium: https://github.com/ashbythorpe/selenider/blob/main/.github/workflows/R-CMD-check-selenium.yaml https://github.com/ashbythorpe/selenider/blob/main/tests/testthat/helper-session.R